200. Luke Skywalker’s Nightmare
Well, I had to do something a little special for my 200th post.
I thought it would be fun to model a space battle, and the TIE fighters are pretty simple – just a sphere suspended between two hexagonal (6 sided) wings.
Modelling the TIE fighter
First of all, I’m not doing anything as complex as that picture. My fighters are small and moving fast, so they only need to look reasonably realistic.
It’s easy to create the hexagonal wings with six mesh triangles. They look a bit flat in the same colour, without the connecting struts you can see above, so I simply coloured alternate triangles in different colours.
The tricky part was the middle – a sphere connected to the wings by (spherical) tubes. Spheres are annoying to draw with vertices. So instead, I cheated. I made a rough circle of vertices, and then connected two of them, one facing forward, and the other facing up (ie at right angles). This means the circles never look flat and 2D. You can see this in the picture below.
Obviously, it’s not too convincing close up, but further away, it starts to look OK – and it will be moving fast, too.
One more thing I did was to angle the wings outward slightly, partly because I noticed some TIE fighters had this, and also because if I don’t, then the wings seem to disappear when the fighters are coming straight at you (remembering the wings don’t have any width).
The final version can be seen below moving about, at various distances. I’m happy with it.
Flying the TIE fighters
First, I spawned a number of fighters at random points in the distance and flew them forwards. It looked really boring, and I could hardly see the wings, and the fighters seemed to sail, instead of darting and swooping, as in the movies. Also, it would be a bit stupid to fly straight at the laser cannons.
So I made them swerve left and right, and up and down, using a sin curve (which is very useful for any kind of up and down motion). All that means is that I used the fact that sin always gives values between -1 and +1, so if I want to turn (say) up to 20 degrees left, then turn back and turn 20 degrees to the right, and go back and forwards, all I need to do is use a formula like
angle = math.sin(ElapsedTime*a + b) * 20
- ElapsedTime is the time since the program started, giving us a timer,
- a controls how fast the angle changes, and
- b is chosen to be (randomly) different for each fighter, so they don’t all turn at the same angle, ie it starts them all at a different place in the turning cycle
The video above shows how this works with two sin curves, one for x and one for y.
I had to fiddle a lot, but I think the result looks good (first video at top).
Aiming and Shooting
I want the TIE fighters to shoot at the camera, when they are pointing close to it.
I think I’ve talked before about how to detect when you are pointing at something in 3D, but here it is anyway. See the numbered notes underneath.
local d=self.pos:dist(camPos) --[1] local a=self.pos+vec3(m[9],m[10],m[11])*d --[2] if a:dist(camPos)<100 then --[3] local p1,p2=Get2Dfrom3D(vec3(0,0,50)),Get2Dfrom3D(a-self.pos) --[4] table.insert(Tie.shots,{p1,p2,ElapsedTime}) --[5] end
[1] First, calculate the distance d from the fighter (self.pos) to the camera (camPos)
[2] Use the model matrix (which contains all the rotations for this fighter, assuming we run the code above after we’ve positioned and rotated our fighter), to calculate the point a which is d pixels in front of of the fighter, in its current direction. It’s a neat trick to do this using just 3 values from the model matrix.
So the point a tells us where the fighter would be, if it was d pixels closer, d also being the distance to the camera.
Why? Because if the fighter is pointing directly at the camera, then a will be exactly at the camera position. So now I can
[3] calculate the distance between a and the camera, and if it is close (say 100 pixels), I am aiming at the camera, and shoot.
Now I have a little problem. It’s difficult to draw a laser beam in 3D. I could use the same cheat as for my fighter, and draw two thick lines at right angles to each other, but they would look very thin when far away, and would be very wide as they got close. Lasers don’t spread out.
The answer is to draw the beams in 2D. We can draw in 2D on top of a 3D screen, but first we have to figure out where our 3D TIE fighter is, on the 2D screen. I have explained this in an earlier post, but here is my code, which is used in the line I marked [4] above.
function Get2Dfrom3D(p) --p is a 3D point local m=modelMatrix()*viewMatrix()*projectionMatrix() m=m:translate(p.x,p.y,p.z) return vec2((m[13]/m[16]+1)*WIDTH/2,(m[14]/m[16]+1)*HEIGHT/2) end
I use this function twice, once to calculate the point a little in front of the fighter (where the laser starts), and the other to calculate the point near the camera where the fighter is pointing.
One small problem is that I don’t want to be switching between 3D and 2D each time I draw a fighter. Far better to draw the 2D lines when I’ve finished all the 2D stuff. So if a fighter is shooting, I put the details in a little table, and use it to draw all the laser beams when I’m finished drawing everything else.
The sky
I’d like some stars in the sky. That should be easy, just draw some white dots on a black background (I use text full stops with random font size, rather than ellipse, which doesn’t go small enough).
Except that in 3D you have to draw the stars behind all the spacecraft, which can be thousands of pixels away. And if I turn the spacecraft, I want stars all around and above and below me, too.
So I used the code I previously found to create a sky globe – a sphere surrounding the camera – and created an image of white dots on black, to act as the stars.
The result
The result is still pretty basic – a bunch of TIE ships flying toward you, firing laser beams – but it’s surprising how hard it is to do in 3D, even if you try to keep it simple. It also shows how worthwhile it is to keep collecting techniques which may come in useful in the future, like the sky globe.
I suppose I should program a laser cannon to fire back and destroy the TIE’s, but I think I’ll enjoy Luke’s nightmare a little longer.
The code
I haven’t posted it because it’s a bit messy, and because I used quaternions to manage rotations (and I was planning to use them for the next stage). Ask if you want it.
The demo looks really cool – well done, what would be really impressive would be to link this with the classic star wars intro text for Star Wars day on May 4th 🙂