/* ParticleCollisionTest.as Lee Felarca http://www.zeropointnine.com/blog 1-5-2007 v0.9 Source code licensed under a Creative Commons Attribution 3.0 License. http://creativecommons.org/licenses/by/3.0/ Some Rights Reserved. */ package { import com.lee.papervision.LeeCamera3D; import com.lee.papervision.PlaneHorizontal; import com.lee.papervision.Util3D; import flash.display.BitmapData; import flash.display.Sprite; import flash.display.StageScaleMode; import flash.events.Event; import flash.events.KeyboardEvent; import flash.text.TextField; import flash.text.TextFormat; import org.papervision3d.cameras.FreeCamera3D; import org.papervision3d.core.geom.renderables.Vertex3D; import org.papervision3d.core.proto.MaterialObject3D; import org.papervision3d.materials.ColorMaterial; import org.papervision3d.materials.WireframeMaterial; import org.papervision3d.materials.utils.MaterialsList; import org.papervision3d.objects.DisplayObject3D; import org.papervision3d.objects.primitives.Cube; import org.papervision3d.render.BasicRenderEngine; import org.papervision3d.scenes.Scene3D; import org.papervision3d.view.Viewport3D; [SWF(width="1066", height="600", frameRate="60", backgroundColor="#FF8C00", pageTitle="Particle Collision Text")] public class ParticleCollisionTest extends Sprite { private const DEGREE:Number = Math.PI/180; private const GRAVITY:Number = 1; private const PLANE_SEGMENTS:int = 10; // MUST BE EVEN! private const PLANE_POINTS:int = PLANE_SEGMENTS + 1; private const PLANE_LENGTH:Number = 1000; private const PLANE_SEGMENT_LENGTH:Number = PLANE_LENGTH / PLANE_SEGMENTS; private const CEILING_HEIGHT:Number = 750; private var scene:Scene3D; private var camLee:LeeCamera3D; private var camFree:FreeCamera3D; private var renderer:BasicRenderEngine; private var viewport:Viewport3D; private var plane:DisplayObject3D; private var tfHelp:TextField; private var ballsD:Array = new Array(); private var shadowsD:Array = new Array(); private var balls:Array = new Array(); private var walls:Array; private var mouseCamera:Boolean = false; private var key:int; private var paused:Boolean; private var camLeeTarget:Vertex3D = new Vertex3D(); // Global in scope because used across core functions: private var velNew:Vertex3D = new Vertex3D(); private var bounceNormal:Vertex3D = new Vertex3D(); // ============================== // INIT-RELATED // ============================== public function ParticleCollisionTest() { init(); } private function init():void { // Stage-related this.stage.scaleMode = StageScaleMode.NO_SCALE; viewport = new Viewport3D(stage.stageWidth, stage.stageHeight, false); this.addChild(viewport); tfHelp = new TextField(); with (tfHelp) { defaultTextFormat = new TextFormat("Arial", 11, 0x666666, true); width = 500; height = 500; mouseEnabled = false; x = 10; y = 20; alpha = 0.5 text = "Particle Collision Detection Test\r\r" + "[Arrow Keys] Move ball laterally\r" + "[Space] Move ball up\r\r" + "[B] Add balls\r\r" + "[Enter] Toggle camera rotation\r" + "[+/-] Move camera in/out\r\r" + "[T] Generate new terrain\r\r" + "[H] Toggle help text"; } this.addChild(tfHelp); // 3d setup scene = new Scene3D(); viewport.opaqueBackground = 0; var matFloor:WireframeMaterial = new WireframeMaterial(0x888888,1,1); matFloor.doubleSided = true; plane = new PlaneHorizontal(matFloor, PLANE_LENGTH,PLANE_LENGTH, PLANE_POINTS-1, PLANE_POINTS-1); camFree = new FreeCamera3D(5); camLee = new LeeCamera3D(new Vertex3D(),500,15,0); scene.addChild(plane); renderer = new BasicRenderEngine(); // WALL BOUNDARIES (not drawn on-screen) var n:Number = PLANE_LENGTH/2; var y:Number = CEILING_HEIGHT; walls = new Array(); // back walls.push( [ new Vertex3D(-n,-y,n), new Vertex3D(n,-y,n), new Vertex3D(-n,y,n) ] ); // counterclockwise walls.push( [ new Vertex3D(n,-y,n), new Vertex3D(n,y,n), new Vertex3D(-n,y,n) ] ); // front walls.push( [ new Vertex3D(-n,-y,-n), new Vertex3D(-n,y,-n), new Vertex3D(n,-y,-n) ] ); walls.push( [ new Vertex3D(n,-y,-n), new Vertex3D(-n,y,-n), new Vertex3D(n,y,-n) ] ); // left walls.push( [ new Vertex3D(-n,-y,n), new Vertex3D(-n,y,-n), new Vertex3D(-n,-y,-n) ] ); walls.push( [ new Vertex3D(-n,-y,n), new Vertex3D(-n,y,n), new Vertex3D(-n,y,-n) ] ); // right walls.push( [ new Vertex3D(n,-y,n), new Vertex3D(n,-y,-n), new Vertex3D(n,y,-n) ] ); walls.push( [ new Vertex3D(n,-y,n), new Vertex3D(n,y,-n), new Vertex3D(n,y,n) ] ); // top walls.push( [ new Vertex3D(n,y,-n), new Vertex3D(-n,y,-n), new Vertex3D(-n,y,n) ] ); walls.push( [ new Vertex3D(n,y,-n), new Vertex3D(-n,y,n), new Vertex3D(n,y,n) ] ); this.addEventListener(Event.ENTER_FRAME, onEnterFrame, false,0,true); this.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown, false,0,true); this.stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp, false,0,true); // Lastly, ... transformFloor(0x000002, true); addBall(); } private function addBall():void { var b:BallVO = new BallVO(); b.pos = new Vertex3D(0,300,0); b.vel = new Vertex3D(Math.random()*10-5, Math.random()*10-5, Math.random()*10-5); balls.push(b); var matBall:MaterialObject3D = new ColorMaterial(0xFFCC00,1); var matListBall:MaterialsList = new MaterialsList(); matListBall.addMaterial( matBall, "all" ); var matShadow:MaterialObject3D = new ColorMaterial(0x888888,0.4); var matListShadow:MaterialsList = new MaterialsList(); matListShadow.addMaterial( matShadow, "all" ); var ball:Cube = new Cube(matListBall, 13,13,13, 1,1,1, 0,0); scene.addChild(ball); ballsD.push(ball); var shadow:Cube = new Cube(matListShadow, 13,13,13, 1,1,1, 0,0); scene.addChild(shadow); shadowsD.push(shadow); } /** * Generate 'heightmap' using BitmapData.perlinNoise, and apply it to our plane */ private function transformFloor($seed:uint, $doBowl:Boolean=false):void { // Create height map source var bmdPerlin:BitmapData = new BitmapData(PLANE_POINTS, PLANE_POINTS, false, 0x0); bmdPerlin.perlinNoise( PLANE_POINTS,PLANE_POINTS, 3, $seed, false, true, 7, true); trace('SEED:',$seed); // Transform floor z values based on height map for (var y:int = 0; y < PLANE_POINTS; y++) { for (var x:int = 0; x < PLANE_POINTS; x++) { var ht:Number = (bmdPerlin.getPixel(x,y) & 0xFF); ht = ht * 3; ht = ht - (256*3/2); setPlanePointHeightAt(plane, x,y, ht); } } // 'Shape' it a little bit if ($doBowl) { var v:Array = plane.geometry.vertices; for (var i:int = 0; i < v.length; i++) { var dist:Number = Util3D.distance( v[i], new Vertex3D() ); v[i].y += dist/4; } } } /** * Helper function for transformFloor() */ private function setPlanePointHeightAt(p:DisplayObject3D, col:int, row:int, ht:Number):void { var i:int = getPlaneIndex(col,row); var v:Vertex3D = p.geometry.vertices[ i ]; v.y = ht; } // ======================================= // CORE ROUTINES // ======================================= private function onEnterFrame(e:Event):void { this.stage.focus = this.stage; if (!paused) { updateBalls(); } updateView(); } private function updateBalls():void { for (var i:int = 0; i < balls.length; i++) { var b:BallVO = balls[i]; updateBall(b, 1) // Leak fix (kludge) var foo:Number = getPlaneHeight(b.pos.x, b.pos.z, true); if (b.pos.y < foo) { b.pos.y = foo; } if (b.pos.y > CEILING_HEIGHT) { trace('ack'); b.pos.y = CEILING_HEIGHT - 50; } if (isNaN(foo)) { var l:Number = PLANE_LENGTH/2; if (b.pos.x < -l) b.pos.x = -l + 1; if (b.pos.x > l) b.pos.x = l - 1; if (b.pos.z < -l) b.pos.z = -l + 1; if (b.pos.z > l) b.pos.z = l - 1; } } } private function updateBall(b:BallVO, $timeUnit:Number):void { velNew = adjustVelocity(b.vel, $timeUnit); var ballDest:Vertex3D = Util3D.add(b.pos, velNew); /* Logic is simplified here in that: (1) For the plane, the triangle where the intersection actually occurs does not _have_ to be the one above ballDest, but might be an adjacent one. I'm not bothering with checking. (2) The collision detect should ideally - but doesn't - recurse thru updateBall. (3) If a collision occurs on more than one polygon, there's no logic here to determine which should be handled first. */ // A. PLANE var p:Array = getTriangle(ballDest.x, ballDest.z); if (p && hitTestAndBounce(b, b.pos, ballDest, p[0],p[1],p[2], $timeUnit)) return; // B. WALLS for (var i:int = 0; i < walls.length; i++) { var t:Array = getTriangle(ballDest.x, ballDest.z); if (hitTestAndBounce(b, b.pos, ballDest, walls[i][0], walls[i][1], walls[i][2], $timeUnit)) return; } // C. AND ANY OTHER POLYGONS... // ... // If we're here, no hit occured, so just advance the ball based on velocity b.vel = velNew.clone(); b.pos = Util3D.add(b.pos, b.vel); } private function hitTestAndBounce(b:BallVO, line0:Vertex3D, line1:Vertex3D, tri0:Vertex3D, tri1:Vertex3D, tri2:Vertex3D, $timeUnit:Number):Boolean { var intersection:Vertex3D = Util3D.lineTriangleIntersect(line0, line1, tri0,tri1,tri2, true); // true?? if (!intersection) return false; var ratio:Number = Util3D.distance(line0, intersection) / Util3D.distance(line0, line1); bounceNormal = Util3D.normal(tri0,tri1,tri2); velNew = adjustVelocity(b.vel, ratio); b.vel = velNew.clone(); b.pos = intersection; handleBounce(b, (1-ratio)*$timeUnit); return true; } private function handleBounce(b:BallVO, $timeUnit:Number):void { velNew = adjustVelocity(b.vel, $timeUnit); // Normalized inverse of the velocity: var velInvNorm:Vertex3D = Util3D.multiply(velNew, -1); velInvNorm = Util3D.normalize(velInvNorm); // Rotate velInvNorm around the plane triangle normal, 180 degrees var bounced:Vertex3D = Util3D.rotateArbitraryAxis(velInvNorm, bounceNormal, 180*DEGREE); velNew = Util3D.multiply(bounced, Util3D.length(b.vel)); // Degrade velocity var m:Number = 50 - Util3D.length(velNew); if (m < 0) m = 0; m /= 250; velNew = Util3D.multiply(velNew, 1-m); b.vel = velNew.clone(); b.pos = Util3D.add(b.pos, b.vel); return; } private function updateView():void { if (mouseCamera) { var x:Number = (stage.mouseX - stage.stageWidth/2) / -80; camLee.yaw += x; var y:Number = (stage.mouseY - stage.stageHeight/2) / -100; camLee.pitch += y; if (camLee.pitch < 0) camLee.pitch = 0; if (camLee.pitch > 90) camLee.pitch = 90; } for (var i:int = 0; i < balls.length; i++) { var b:BallVO = balls[i]; Util3D.setPos(ballsD[i], b.pos); var sy:Number = getPlaneHeight(b.pos.x, b.pos.z, true); Util3D.setPos(shadowsD[i], new Vertex3D(b.pos.x, sy, b.pos.z)); } var midY:Number = getPlaneHeight(balls[0].pos.x, balls[0].pos.z, true); midY = (balls[0].pos.y - midY) * 0.75; var midPt:Vertex3D = new Vertex3D(balls[0].pos.x, midY, balls[0].pos.z); camLeeTarget.x += (midPt.x - camLeeTarget.x) / 7; camLeeTarget.y += (midPt.y - camLeeTarget.y) / 7; camLeeTarget.z += (midPt.z - camLeeTarget.z) / 7; camLee.target = camLeeTarget; camLee.toFreeCamera3D(camFree); renderer.renderScene(scene, camFree, viewport); } // CORE ROUTINES END // ====================================== /** * Adjust velocity for a particle over a unit of time * (_except_ for collisions) */ private function adjustVelocity($v:Vertex3D, $timeUnit:Number):Vertex3D { var v:Vertex3D = $v.clone(); // User input affects velocity if (key > 0) { var u:Vertex3D = userAffectedVelocity(); if (u) { u = Util3D.rotY(u, camLee.yaw*DEGREE); u = Util3D.multiply(u, $timeUnit); v = Util3D.add(v, u); } } // Gravity affects velocity if (Util3D.length(v) != 0) v.y -= (GRAVITY * $timeUnit); // Degrade velocity var degrade:Number = 1 - (0.006 * $timeUnit); v = Util3D.multiply(v, degrade); return v; } private function userAffectedVelocity():Vertex3D { var v:Vertex3D; if (key==32) { v = new Vertex3D(0,5,0); } else if (key==38) { // up v = new Vertex3D(0,0,2); } else if (key==40) { // down v = new Vertex3D(0,0,-2); } else if (key==37) { // left v = new Vertex3D(-2,0,0); } else if (key==39) { // right v = new Vertex3D(2,0,0); } return v; } private function getPlaneIndex(col:int, row:int):int { return (col * PLANE_POINTS) + row; } /** * Returns the height of the _horizontal_ plane at a given x/z coordinate. * Makes assumption that the plane has equally spaced vertices, etc. */ private function getPlaneHeight(x:Number, z:Number, treatTriangleAsPlane:Boolean=false):Number { var a:Array = getTriangle(x,z); if (!a) { return undefined; } var rayA:Vertex3D = new Vertex3D(x, -1000, z); var rayB:Vertex3D = new Vertex3D(x, 1000, z); var hit:Vertex3D = Util3D.lineTriangleIntersect(rayA, rayB, a[0], a[1], a[2], treatTriangleAsPlane); if (!hit) return undefined; return hit.y; } /** * Returns the 3 vertices of the triangle of the _horizontal_ plane encompassing a given x/z coordinate * Makes assumption that the plane has equally spaced vertices, etc. */ private function getTriangle(x:Number, z:Number):Array { var col:int = int( x / PLANE_SEGMENT_LENGTH); var row:int = int( z / PLANE_SEGMENT_LENGTH); if (x < 0) col -= 1; if (z < 0) row -= 1; col += PLANE_SEGMENTS/2; // PLANE_SEGMENTS must be an even number! row += PLANE_SEGMENTS/2; if (col < 0 || row < 0 || col >= PLANE_SEGMENTS || row >= PLANE_SEGMENTS) return undefined; var vA:Vertex3D = plane.geometry.vertices[ getPlaneIndex(col+0,row+0) ]; var vB:Vertex3D = plane.geometry.vertices[ getPlaneIndex(col+1,row+0) ]; var vC:Vertex3D = plane.geometry.vertices[ getPlaneIndex(col+0,row+1) ]; var vD:Vertex3D = plane.geometry.vertices[ getPlaneIndex(col+1,row+1) ]; // determine which triangle (lower left or upper right) var closerTriangle:Boolean = false; if ( (x - vA.x) + (z - vA.z) < PLANE_SEGMENT_LENGTH ) closerTriangle = true; // a ray pointing straight down if (closerTriangle) return [vA,vB,vC]; // clockwise! else return [vB,vD,vC]; } private function onKeyUp(e:KeyboardEvent):void { if (key==84) // t transformFloor(uint(Math.random()* 0xFFFFFF), true); else if (key==66) // b addBall(); else if (key==80) { // p paused = !paused; } else if (key==13) { // enter mouseCamera = ! mouseCamera; } else if (key==187) { // plus camLee.distance *= 1/1.2; } else if (key==189) { // minus camLee.distance *= 1.2; } else if (key==72) { // h tfHelp.visible = !tfHelp.visible; } key = 0; } private function onKeyDown(e:KeyboardEvent):void { key = e.keyCode; } } } // ===================================== import org.papervision3d.core.geom.renderables.Vertex3D; class BallVO { public var pos:Vertex3D; public var vel:Vertex3D; public function BallVO() { pos = new Vertex3D; vel = new Vertex3D; } }