### Author Topic: Lightsaber Test  (Read 5910 times)

#### AGP

• Posts: 1646
##### Lightsaber Test
« on: October 22, 2009, 07:28:17 pm »
I was going to put this little example in the wiki anyway, but something strange is happening. The very simple premise is that at 0-degree x rotation, the lightsaber pointed up, the lightsaber's blade (a transparent plane on top of a 3d cylinder) is at its full height. At 90 degrees, it's invisible. So for every degree you rotate it, it shrinks height/90. That was going to work for the first 90 degrees, and I would apply it appropriately for each of the other three sets of 90, and then apply it on the second axis (it never needs to rotate on all 3). To prove that everything works I'm drawing squares around the edges of the blade and I wrote a shrinkOrGrow() method which successfully fully shrinks the blade plane and extends it again to its full height. Yet when I rotate the lightsaber by 10 degrees and shrink the plane by unitPerDegree*10, the plane doesn't shrink far enough. The lightsaber is Obi-wan's off of scifi3d.com.

Code: [Select]
`/*AGP, 2009*/import java.awt.*;import java.awt.event.*;import com.threed.jpct.*;import com.threed.jpct.util.*;public class LightsaberTest extends Frame implements WindowListener, KeyListener {     private World theWorld;     private FrameBuffer buffer;     private Camera theCamera;     private boolean keepGoing;     private Graphics g;     private Light light;     private Canvas canvasGL;     private Lightsaber lightsaber;     protected float xRotation;     public LightsaberTest() { this.setTitle("BR's"); xRotation = 0.0f; keepGoing = true; Config.maxPolysVisible *= 16; theWorld = new World(); lightsaber = new Lightsaber(theWorld); theCamera = theWorld.getCamera(); buffer = new FrameBuffer(1024, 768, FrameBuffer.SAMPLINGMODE_NORMAL); canvasGL = new Canvas();// canvasGL = buffer.enableGLCanvasRenderer();// buffer.disableRenderer(IRenderer.RENDERER_SOFTWARE); this.add(canvasGL, BorderLayout.CENTER); theWorld.setAmbientLight(145, 145, 145); theCamera.setPosition(lightsaber.getTransformedCenter()); light = new Light(theWorld); light.setIntensity(35, 35, 35); light.setAttenuation(8f); theCamera.moveCamera(Camera.CAMERA_MOVEOUT, 25f); theCamera.lookAt(lightsaber.getTransformedCenter()); light.setPosition(theCamera.getPosition()); canvasGL.addKeyListener(this); this.addKeyListener(this); this.addWindowListener(this); this.setSize(1024, 768); this.setVisible(true); drawLoop();     }     private void drawLoop() { g = this.getGraphics(); while (keepGoing) {      try { Thread.sleep(50);      }      catch (InterruptedException e) {} if (canvasGL.getGraphics() == null) continue;      buffer.clear();      lightsaber.rotateDegreesX(xRotation);      theWorld.renderScene(buffer);      theWorld.draw(buffer);//      buffer.display(g); buffer.display(canvasGL.getGraphics()); lightsaber.drawEdges(theCamera, buffer, (Graphics2D)canvasGL.getGraphics());// buffer.displayGLOnly();// canvasGL.paint(canvasGL.getGraphics());//NEEDED ONLY BECAUSE displayGLOnly() DOESN'T GO AS FAR AS display() }     }     public void windowClosing(WindowEvent e) { if (e.getWindow() == this) {      keepGoing = false;      this.dispose();      System.exit(0); }     }     public void windowClosed(WindowEvent e) {}     public void windowOpened(WindowEvent e) {}     public void windowActivated(WindowEvent e) {}     public void windowDeactivated(WindowEvent e) {}     public void windowIconified(WindowEvent e) {}     public void windowDeiconified(WindowEvent e) {}     public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); if (e.isShiftDown() && keyCode == KeyEvent.VK_UP)      theCamera.moveCamera(Camera.CAMERA_MOVEIN, 5f); else if (e.isShiftDown() && keyCode == KeyEvent.VK_DOWN)      theCamera.moveCamera(Camera.CAMERA_MOVEOUT, 5f); else if (keyCode == KeyEvent.VK_UP)      xRotation += 10f; else if (keyCode == KeyEvent.VK_DOWN)      xRotation -= 10f; else if (keyCode == KeyEvent.VK_G)      lightsaber.bladeController.shrinkOrGrow();     }     public void keyReleased(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN)      xRotation = 0f;     }     public void keyTyped(KeyEvent e) {}     public static void main(String[] args) { new LightsaberTest();      }}class Lightsaber {     private Object3D lightsaber, bladePlane;     protected VertexController bladeController;     private float totalX;     public Lightsaber(World theWorld) { totalX = 0.00f; TextureManager.getInstance().addTexture("PLATEOX2.JPG", new Texture("PLATEOX2.JPG")); TextureManager.getInstance().addTexture("scratch.jpg", new Texture("scratch.jpg")); lightsaber = Object3D.mergeAll(Loader.load3DS("saber.3ds", .1f)); TextureManager.getInstance().addTexture("Blade2.jpg", new Texture("Blade2.jpg")); bladePlane = Primitives.getPlane(1, 8); bladePlane.setTransparency(0); bladePlane.setTransparencyMode(Object3D.TRANSPARENCY_MODE_ADD); bladePlane.setTexture("Blade2.jpg"); bladePlane.setBillboarding(true); theWorld.addObject(lightsaber); theWorld.addObject(bladePlane); theWorld.buildAllObjects(); bladePlane.translate(-1.4f, -19f, 0); bladeController = new VertexController(bladePlane); bladeController.scaleYandShift(8f);     }     public SimpleVector getTransformedCenter() { return lightsaber.getTransformedCenter();//CHANGE LATER TO AVERAGE GRIP'S AND BLADE'S CENTERS     }     private void toWorldSpace(SimpleVector objectSpace) { SimpleVector translation = bladePlane.getTranslation(); objectSpace.x += translation.x; objectSpace.y += translation.y; objectSpace.z += translation.z;     }     public void drawEdges(Camera cam, FrameBuffer buffer, Graphics2D g) { bladeController.refreshMeshData(); VectorAndIndex[] topMost = bladeController.getTopMost(); toWorldSpace(topMost[0].vector=new SimpleVector(topMost[0].vector)); toWorldSpace(topMost[1].vector=new SimpleVector(topMost[1].vector)); SimpleVector[] top2D = new SimpleVector[]{Interact2D.project3D2D(cam, buffer, topMost[0].vector), Interact2D.project3D2D(cam, buffer, topMost[1].vector)}; VectorAndIndex[] bottomMost = bladeController.getBottomMost(); toWorldSpace(bottomMost[0].vector=new SimpleVector(bottomMost[0].vector)); toWorldSpace(bottomMost[1].vector=new SimpleVector(bottomMost[1].vector)); SimpleVector[] bottom2D = new SimpleVector[]{Interact2D.project3D2D(cam, buffer, bottomMost[0].vector), Interact2D.project3D2D(cam, buffer, bottomMost[1].vector)}; if (bottom2D[0].y > 0) {      g.setColor(Color.red);      g.drawRect((int)top2D[0].x-10, (int)top2D[0].y-10, 20, 20);      g.drawRect((int)top2D[1].x-10, (int)top2D[1].y-10, 20, 20);      g.setColor(new Color(255, 0, 128));      g.drawRect((int)bottom2D[0].x-10, (int)bottom2D[0].y-10, 20, 20);      g.drawRect((int)bottom2D[1].x-10, (int)bottom2D[1].y-10, 20, 20); }     }     public void rotateDegreesX(float xRotation) {//.05 RADIANS==2.8647º, 90 DEGREES==1.5707 RADIANS float rotationRadians = (float) Math.toRadians(xRotation); lightsaber.rotateAxis(lightsaber.getXAxis(), rotationRadians); if (xRotation != 0.0f) {      totalX += xRotation;      bladeController.shrinkByDegrees(xRotation); }     }}class VertexController extends GenericVertexController {     protected Mesh controlled;     private float originalWidth = 0f;     private float unitPerDegree;//THE AMOUNT BY WHICH THE PLACE SHOULD SHRINK PER DEGREE's ROTATION     private boolean activated;     private float originalHeight;     public VertexController(Object3D toControl) { controlled = toControl.getMesh(); this.init(controlled, true); unitPerDegree = getHeight()/90.0f; activated = true; System.out.println("Height: "+getHeight() +", unitPerDegree: "+unitPerDegree);     }     public void scaleYandShift(float scale) { SimpleVector[] vertices = this.getSourceMesh(); SimpleVector[] destination = this.getDestinationMesh(); VectorAndIndex[] topMost = getTopMost(); float height = getHeight(); topMost[0].vector.y -= height*scale; topMost[1].vector.y -= height*scale; destination[topMost[0].index] = topMost[0].vector; destination[topMost[1].index] = topMost[1].vector; this.updateMesh(); originalHeight = getHeight();     }     public void shrinkOrGrow() { VectorAndIndex[] topMost = getTopMost(); SimpleVector[] destination = this.getDestinationMesh(); if (activated) {     float height = getHeight();     destination[topMost[0].index].y += height-.05;     destination[topMost[1].index].y += height-.05; } else {     destination[topMost[0].index].y -= originalHeight+.05;     destination[topMost[1].index].y -= originalHeight+.05;     } activated = !activated; this.updateMesh();     }     public void shrinkByDegrees(float degrees) { VectorAndIndex[] topMost = getTopMost(); SimpleVector[] destination = this.getDestinationMesh(); System.out.println("Shrink factor: "+(unitPerDegree*degrees)); destination[topMost[0].index].y += (unitPerDegree*degrees); destination[topMost[1].index].y += (unitPerDegree*degrees); this.updateMesh();     }     protected VectorAndIndex[] getTopMost() {//POSITIVE y GOES *DOWN* VectorAndIndex[] topMost = new VectorAndIndex[2]; SimpleVector[] vertices = this.getSourceMesh(); topMost[0] = new VectorAndIndex(vertices[0], 0); for (int i = 1; i < vertices.length; i++)      if (topMost[0].vector.y > vertices[i].y) topMost[0] = new VectorAndIndex(vertices[i], i); topMost[1] = new VectorAndIndex(vertices[0], 0); for (int i = 1; i < vertices.length; i++)      if (topMost[1].vector.y >= vertices[i].y && topMost[0].index != i) topMost[1] = new VectorAndIndex(vertices[i], i); return topMost;     }     public float getHeight() { VectorAndIndex[] topMost = getTopMost(); VectorAndIndex[] bottomMost = getBottomMost(); float height = bottomMost[0].vector.y-topMost[0].vector.y; return height;     }     public void apply() {}     protected VectorAndIndex[] getBottomMost() {//POSITIVE y GOES *DOWN* VectorAndIndex[] bottomMost = new VectorAndIndex[2]; SimpleVector[] vertices = this.getSourceMesh(); bottomMost[0] = new VectorAndIndex(vertices[0], 0); for (int i = 1; i < vertices.length; i++)      if (bottomMost[0].vector.y < vertices[i].y) bottomMost[0] = new VectorAndIndex(vertices[i], i); bottomMost[1] = new VectorAndIndex(vertices[0], 0); for (int i = 1; i < vertices.length; i++)      if (bottomMost[1].vector.y <= vertices[i].y && bottomMost[1].index != i) bottomMost[1] = new VectorAndIndex(vertices[i], i); return bottomMost;     }}class VectorAndIndex {     protected SimpleVector vector;     protected int index;     public VectorAndIndex(SimpleVector vector, int index) { this.vector = vector; this.index = index;     }}`

#### AGP

• Posts: 1646
##### Re: Lightsaber Test
« Reply #1 on: October 22, 2009, 07:38:32 pm »
By the way, I know that the pivot isn't perfectly set so that the lower end of the blade, after several rotations, needs to be adjusted as well. I was going to do that last.

#### paulscode

• double
• Posts: 863
##### Re: Lightsaber Test
« Reply #2 on: October 22, 2009, 09:40:12 pm »
This isn't an answer to your specific problem, but I think I wrote a single-axis billboarding method a while back to use for cylindrical special effects (should work for a lightsaber glow effect).  I can dig up that method for you if you are interested.  It might be a viable alternative to achieve the same effect you are trying to simulate here.

#### AGP

• Posts: 1646
##### Re: Lightsaber Test
« Reply #3 on: October 22, 2009, 09:55:59 pm »
Sure, thanks a lot, show me what you've got.

But I'd still like to know what's wrong with mine (nothing, as far as I can tell!).

#### paulscode

• double
• Posts: 863
##### Re: Lightsaber Test
« Reply #4 on: October 23, 2009, 02:43:04 am »
Ok, I tried to clean the method up a little bit:
Code: [Select]
`    private void singleAxisBillboard( Object3D parentObject,                                      Object3D childBillboard,                                      SimpleVector targetPoint )    {        // Get parent object's world rotation matrix:        Matrix m = new Matrix( parentObject.getWorldTransformation() );        float[] dm = m.getDump();        for( int i = 12; i < 15; i++ )        {            dm[i] = 0;        }        dm[15] = 1;        m.setDump( dm );        // Get the inverse:        Matrix i = new Matrix( m ).invert3x3();        // Convert target point into parent's Object Space:        SimpleVector convertedTarget = targetPoint.calcSub(                                          parentObject.getTransformedCenter() );        convertedTarget.matMul( i );        // Cast target onto root object's x/z plane (Object Space):        convertedTarget.y = 0;        // Calculate the "up" and "look" directions (Object Space):        SimpleVector up = new SimpleVector( 0, -1, 0 );        SimpleVector look = convertedTarget.normalize();        // ** NOTE: Use this if childBillboard is not a child of parentObject **        // Convert to World Space directions:        //  up.matMul( m );        //  up = up.normalize();        //  look.matMul( m );        //  look = look.normalize();        // **        // Calculate the "right" direction:        SimpleVector right = up.calcCross( look ).normalize();        // Create the destination rotation matrix:        Matrix destMatrix = new Matrix();        destMatrix.set( 0, 0, right.x );        destMatrix.set( 1, 0, up.x );        destMatrix.set( 2, 0, look.x );        destMatrix.set( 3, 0, 0.0f );        destMatrix.set( 0, 1, right.y );        destMatrix.set( 1, 1, up.y );        destMatrix.set( 2, 1, look.y );        destMatrix.set( 3, 1, 0.0f );        destMatrix.set( 0, 2, right.z );        destMatrix.set( 1, 2, up.z );        destMatrix.set( 2, 2, look.z );        destMatrix.set( 3, 2, 0.0f );        destMatrix.set( 0, 3, 0.0f );        destMatrix.set( 1, 3, 0.0f );        destMatrix.set( 2, 3, 0.0f );        destMatrix.set( 3, 3, 1.0f );        // ** NOTE: Use this if childBillboard is not a child of parentObject **        //  childBillboard.translate( rootObject.getTransformedCenter().calcSub(        //      myBillboard.getTransformedCenter() ) );        // **        childBillboard.setRotationMatrix( destMatrix );    }`
The method assumes that childBillboard is a child of parentObject.  If that is not the case in your example, see the comments in the code for how to change it.  It should at least be enough to get you started.

I created a simple demo applet:
http://www.paulscode.com/source/Glowing/TubeLightEffect.html

As you can see in the above demo, this effect works well for cylindars as long as you aren't looking straight down one of the ends.  If you need to fix this, you could try placing something on the ends to hide the edge or maybe adding a "cap" of some kind to the billboard itself.

#### AGP

• Posts: 1646
##### Re: Lightsaber Test
« Reply #5 on: October 23, 2009, 04:25:25 am »
I know I said I was going to read a book on matrices, but I ended up reading (still am) one on game AI and finishing The New Jedi Order.  I did get one and it's my next read. I understand your general idea, but I'd really like to make mine work.

I would cap it with an intersecting plane if I were you. Thanks for digging this up, it's always good looking at different approaches.

#### EgonOlsen

• Posts: 12065
##### Re: Lightsaber Test
« Reply #6 on: October 23, 2009, 09:30:15 am »
Could post some screen shots that illustrate the problem?

#### AGP

• Posts: 1646
##### Re: Lightsaber Test
« Reply #7 on: October 23, 2009, 06:42:42 pm »
Here you go:

#### EgonOlsen

• Posts: 12065
##### Re: Lightsaber Test
« Reply #8 on: October 23, 2009, 11:13:05 pm »
Well...ok...and the exact problem is? From that screen shot, it's hard to tell. I've looked at your code, but it's hard to find out what may be wrong if i'm not sure what exactly you want to achieve....

#### AGP

• Posts: 1646
##### Re: Lightsaber Test
« Reply #9 on: October 24, 2009, 12:37:39 am »
I wrote it in the first post: for every x degree I turn I should need only translate top most vertices by height/90, right? Only as you can see in the screenshot, it doesn't shrink enough.

#### paulscode

• double
• Posts: 863
##### Re: Lightsaber Test
« Reply #10 on: October 24, 2009, 01:03:05 am »
I would put in a few more calls to println at strategic points in the code to display varriable values along the way.  Then you can check the console output and use a calculator to make sure those numbers jive with what you expect them to be.  It can at least give you an idea of where the problem is not at.

#### AGP

• Posts: 1646
##### Re: Lightsaber Test
« Reply #11 on: October 24, 2009, 01:41:21 am »
That's always good advice, but I already have everywhere, the code is really simple, and I already proved I can shrink and re-extent the glow at will.

#### paulscode

• double
• Posts: 863
##### Re: Lightsaber Test
« Reply #12 on: October 24, 2009, 03:13:45 am »
So having verified that the numbers add up the way they should, and having verified that the glow effect resizing works properly when given the proper angles, then the problem must be with the light sabre and not with the glow effect, correct?  In other words, the light sabre is shorter than it should be, not that the glow effect is longer than it should be.  That is strange.  One thing you might check is if the visible height of the blade after rotation is a linear function of the rotation angle.  In other words, you know that 0 degrees is full height, and 90 degrees is zero height, but is 45 degrees half height?  (I'd check this for the glow effect resizing function too if you haven't already).  It seems unlikely that this is the problem, but it might be worth while to actually measure it to be sure.  I know depth figures into the 3D equation to some extent, so since the tip is further away from the camera than the handle is, that might cause a noticable height difference than what you are expecting it to be.

#### AGP

• Posts: 1646
##### Re: Lightsaber Test
« Reply #13 on: October 24, 2009, 05:55:06 am »
That's actually quite brilliant, thank you. Now that you mentioned it, it does seem likely that it's not linear, given the distances. I'll check and report back.