<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-2353779988231261722</id><updated>2011-11-28T12:01:56.856-08:00</updated><category term='knowledgebin'/><category term='i-bloody-tunes'/><category term='firefox'/><category term='xbox'/><category term='XSLT'/><category term='3D'/><category term='Java'/><category term='R.E.M.'/><category term='Linux'/><category term='l33t h4x0r'/><category term='Music'/><title type='text'>Good Advices</title><subtitle type='html'>When you meet a stranger, look at his code</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default?max-results=100'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>31</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>100</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-1293994408529612576</id><published>2011-11-28T12:00:00.001-08:00</published><updated>2011-11-28T12:01:56.863-08:00</updated><title type='text'>Insert Post Here</title><content type='html'>Insert shame-faced regret at not updating blog more here&lt;insert blog="" for="" here="" more="" not="" regret="" shame-faced="" updating=""&gt;&lt;/insert&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-1293994408529612576?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/1293994408529612576/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=1293994408529612576' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/1293994408529612576'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/1293994408529612576'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2011/11/blog-post.html' title='Insert Post Here'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-4061381905425895382</id><published>2010-11-12T12:44:00.000-08:00</published><updated>2010-11-12T12:44:56.404-08:00</updated><title type='text'>Please release me</title><content type='html'>Ship it, ship it, ship it! Spiff v0.1.0 (yes, I'm being cautious) out now, you can grab the jar from&amp;nbsp;&lt;a href="https://github.com/revbingo/SPIFF/downloads"&gt;https://github.com/revbingo/SPIFF/downloads&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-4061381905425895382?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/4061381905425895382/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=4061381905425895382' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/4061381905425895382'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/4061381905425895382'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2010/11/please-release-me.html' title='Please release me'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-256061406099817146</id><published>2010-09-21T22:01:00.000-07:00</published><updated>2010-09-22T04:30:29.601-07:00</updated><title type='text'>Spiff: The competition</title><content type='html'>In the intervening years between the first inception of Spiff (yes, I'm going camel case, upper case everywhere just won't do) and it's subsequent revival (and re-revival), a couple of competitors have appeared in the same space.&lt;br /&gt;&lt;br /&gt;Closest in spirit to Spiff is &lt;a href="http://preon.codehaus.org/"&gt;Preon&lt;/a&gt;. It even states it's aim "to be to binary encoded data what Hibernate is to relational databases, and JAXB to XML", which pretty much sums up what I want with Spiff. Where Preon differs is in it's extensive use of annotations to do what Spiff does in it's format definition file (which Spiff calls an .adf file - Arbitrary Data Format).  Preon will examine your classes and derive the data format from the order and types of annotated fields in the class. It also uses annotations to derive looping and conditional logic.&lt;br /&gt;&lt;br /&gt;I'll admit that I've not used Preon - partly through fear of polluting my ideas about what Spiff could and should do, and partly in case I decided it was better than Spiff and just decided to call the whole thing off.  So any discussion of it's merits and drawbacks are truly superficial. My general impression is that it's reliance on annotations are a little hairy - when you're expressing logic in annotations, things look a little awkward.  Spiff trades off compactness (having everything described in the code) for readability and also portability.  The event dispatching and class binding mechanism means that one .adf file can be used to populate classes of any shape without needing to respecify the file format.  This also highlights the fact that it looks like Preon expects the classes to fully describe the file format, which is rarely what you want in the code.  One of the use cases that led to me starting to write Spiff was wanting to get little pieces of the data without having to worry about the rest of the file format.  And thus were &lt;code&gt;.jump&lt;/code&gt; and &lt;code&gt;.skip&lt;/code&gt; begat.&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;On the other side, the Google lads are also in the frame with &lt;a href="http://code.google.com/p/protobuf/"&gt;protobuf&lt;/a&gt;.   Protobuf is interesting in that it uses something analogous to the .adf file to describe the format.  Where it differs is that it will generate classes for you to serialize and deserialize the format.  That's something that Spiff might be capable of one day, but I like the idea of being able to write arbitrary POJOs and map the data onto them, rather than having objects in my code whose sole purpose is as marshallers. Also, it's largely oriented towards message-passing, that is, describing a message that will be passed between two systems, such as in RPC, where the user is in control of both ends of the transaction. To that end, the .proto definitions are reliant on using the underlying protobuf grammar for the message, for instance to recognise repeated blocks of data, and don't have some of the flexibility to express more complicated relationships between parts of the file. &lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;I see a couple of strong points in Spiff from this.  Portability of .adf files is possibly the biggest. Once someone has defined an .adf for, say, a .bmp file, or an ItunesDB file, anyone else can take that and use it to bind all or part of that data to their own classes.  The other is flexibility, in hopefully being able to express all the things that can make binary file formats tricky to work with.  I guess first step is to have a working product...&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-256061406099817146?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/256061406099817146/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=256061406099817146' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/256061406099817146'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/256061406099817146'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2010/01/spiff-competition.html' title='Spiff: The competition'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-7245135173893712023</id><published>2010-09-21T21:45:00.000-07:00</published><updated>2010-09-22T04:29:03.253-07:00</updated><title type='text'>Spiff!</title><content type='html'>Ok, I'm back on Spiff. I mean it this time.  Repeat after me - "I will ship software, I will ship software, I will ship software".&lt;br /&gt;&lt;br /&gt;Coming back to code after a little time is an interesting experience. Almost every time you can guarantee a few nuggets of insight that hadn't occurred previously.&lt;br /&gt;&lt;br /&gt;Today's lesson: if it's difficult writing a unit test for (usual suspects being the &lt;code&gt;FileNotFoundException&lt;/code&gt;s and &lt;code&gt;if(x == null)&lt;/code&gt;) conditions), it's probably not worth having in the code.&lt;br /&gt;&lt;br /&gt;I'm not normally one for striving for 100% code coverage with tests. Like all things that fall under the agile/XP umbrella, if you're doing it by the book, you're doing it wrong.  There isn't a book that tells you how &lt;span style="font-weight:bold;"&gt;you&lt;/span&gt; should be working on &lt;span style="font-weight:bold;"&gt;your&lt;/span&gt; projects.&lt;br /&gt;&lt;br /&gt;However, in this instance, I thought it would be an interesting exercise to try and get up to 100%. By getting OCD on the unit tests, I found at least two conditions in my code that couldn't actually occur:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;a null check on an object right after it's constructor was called, and &lt;/li&gt;&lt;li&gt;an exception thrown from code where I was using dynamic assignment where static assignment was sufficient.&lt;/li&gt;&lt;/ul&gt;In the latter case, I had&lt;br /&gt;&lt;pre&gt;try {&lt;br /&gt;   lib = Class.forName("java.lang.Math");&lt;br /&gt;} catch (ClassNotFoundException e) {&lt;br /&gt;   //how do I get here?&lt;br /&gt;}&lt;/pre&gt;Can you write a test that exercises the catch block?  Nope.  This was a remnant of old code that hadn't been cleaned up. What I should have been doing was&lt;pre&gt;lib = java.lang.Math.class;&lt;/pre&gt;&lt;div&gt;which doesn't throw any exception.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;It's good to remember that adding test cases is not the only way to get closer to 100% code coverage - deleting code does just as well.&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-7245135173893712023?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/7245135173893712023/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=7245135173893712023' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/7245135173893712023'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/7245135173893712023'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2010/08/spiff.html' title='Spiff!'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-5655117271714506070</id><published>2010-09-21T13:48:00.000-07:00</published><updated>2010-09-22T04:27:25.748-07:00</updated><title type='text'>How final is final?</title><content type='html'>One interesting tidbit from Spiff development.  How final is &lt;pre&gt;final int x = 1&lt;/pre&gt;? Answer: Not very, if you're using reflection.  &lt;code&gt;Field.setAccessible(true)&lt;/code&gt; will soon get you round any awkward encapsulation issues.&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;So, newly armed with that knowledge, what's printed out here?&lt;br /&gt;&lt;pre&gt;public class HowFinal {&lt;br /&gt;    private final int x = 1;&lt;br /&gt;    &lt;br /&gt;    public static void main(String[] args) throws Exception {&lt;br /&gt;        HowFinal howFinal = new HowFinal();&lt;br /&gt;        Field f = howFinal.getClass().getDeclaredField("x");&lt;br /&gt;        f.setAccessible(true);&lt;br /&gt;        f.set(howFinal,2);&lt;br /&gt;        System.out.println(howFinal.getX());&lt;br /&gt;        System.out.println(f.get(howFinal));&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    public int getX() {&lt;br /&gt;        return x;&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;The answer, unexpectedly, is &lt;br /&gt;&lt;pre&gt;1&lt;br /&gt;2&lt;/pre&gt;&lt;br /&gt;Er, so x was final after all?  Sort of. The compiler inlines constants at compile time, so as far as the runtime JVM is concerned, &lt;code&gt;getX()&lt;/code&gt; contains the code &lt;code&gt;return 1;&lt;/code&gt;.  Querying the field via reflection shows it's true value of 2.&lt;br /&gt;&lt;br /&gt;Is there any question whose answer doesn't start with "it depends"?&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-5655117271714506070?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/5655117271714506070/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=5655117271714506070' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/5655117271714506070'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/5655117271714506070'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2010/09/how-final-is-final.html' title='How final is final?'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-2886846069524998237</id><published>2010-09-21T12:57:00.001-07:00</published><updated>2010-09-21T13:00:09.786-07:00</updated><title type='text'>Spiff on Github</title><content type='html'>Spiff is now available to &lt;a href="http://github.com/revbingo/SPIFF"&gt;download/fork/whatever&lt;/a&gt; at GitHub.  The GitHub site also has a wiki with some instructions for getting started.&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Current state is pre-pre-pre-pre-alpha.  That is, it doesn't really work, I'm rebuilding some of the core parts, but it's there for anyone who wants to snoop.  Ship early, ship often!&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-2886846069524998237?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/2886846069524998237/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=2886846069524998237' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/2886846069524998237'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/2886846069524998237'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2010/09/spiff-on-github.html' title='Spiff on Github'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-1142794722225106425</id><published>2010-01-10T00:56:00.000-08:00</published><updated>2010-01-10T07:00:29.086-08:00</updated><title type='text'>In (Re-)Development: SPIFF</title><content type='html'>&lt;a href="http://diveintomark.org/archives/2002/04/10/shipping_is_a_feature"&gt;Shipping is a Feature&lt;/a&gt;. A feature that, to be fair, most (all) of my personal software projects have dropped from scope at some point along the way.&lt;br /&gt;&lt;br /&gt;One such project was &lt;a href="http://code.assembla.com/revbingo/subversion/nodes/SPIFF?rev=46"&gt;SPIFF&lt;/a&gt;. Originally the less pronounceable "SPADF" (Simple Parser for Aribtrary Data Formats), and developed about 4 years ago, the aim was to have a simple way to get data out of those nasty binary file formats that employ obscure things like, y'know, &lt;i&gt;bytes&lt;/i&gt;. I mean, why store your integer in 4 bytes when you could wrap it in an xml file?&lt;br /&gt;&lt;pre&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&lt;br /&gt;&amp;lt;root&amp;gt;&lt;br /&gt;    &amp;lt;data type=&amp;quot;integer&amp;quot;&amp;gt;1&amp;lt;/data&amp;gt;&lt;br /&gt;&amp;lt;/root&amp;gt;&lt;/pre&gt;&lt;br /&gt;At the time, I was trying to parse out id3 tags from MP3 files, and compare them to data in an ITunesDB file, both of which employ such ruthless, egregious efficiency. In trying to implement parsers for both these formats, the alarm bells told me that there  had to be an easier way.  What I wanted to do was define the data format and it's rules in a simple way, and then have some standard bit of code do the hard work.  At that time, my desire was to have something like a SAX parser, to which I could listen for events and do work appropriately when the data I needed popped out.&lt;br /&gt;&lt;br /&gt;At the heart of it was a &lt;a href="https://javacc.dev.java.net/"&gt;JavaCC&lt;/a&gt; generated parser, which would read a file that defined a data format:&lt;br /&gt;&lt;pre&gt;int                     headerSize&lt;br /&gt;short                   version&lt;br /&gt;byte                    flags&lt;br /&gt;int                     stringLength&lt;br /&gt;string(stringLength)    description&lt;/pre&gt;&lt;br /&gt;and spit out a sequence of Instruction objects that knew how to parse each of these things, and fire events as necessary. Of course, file formats aren't that simple. Even in the basic example above, there's a need to evaluate one element based on the value of another - the description field has a length defined in the int that comes before it.  Likewise, in any non-trivial file format, there's a need for conditionals, loops and jumps, almost always based on a value from elsewhere in the file. SPIFF lets users define these these using a dot in front of keywords:&lt;br /&gt;&lt;pre&gt;.if(version==1.0) {&lt;br /&gt;   byte    flags&lt;br /&gt;} .else {&lt;br /&gt;   short   biggerFlags&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;a href="http://www.gnu.org/software/jel/"&gt;Jel&lt;/a&gt; is employed under the covers to deal with evaluation of expressions. Any values that have been defined previously in the file can be used in an expression, and the ampersand can be used to reference the position of that value in the file. For instance, in a 24-bit &lt;a href="http://en.wikipedia.org/wiki/BMP_file_format"&gt;bitmap&lt;/a&gt;, each "row" of pixels (defined by 3 bytes) is padded to a boundary on a multiple of 4. So it's necessary to do things like:&lt;br /&gt;&lt;pre&gt;.repeat(pixelHeight) {&lt;br /&gt;    .mark(startOfRow)&lt;br /&gt;    .repeat(pixelWidth) {&lt;br /&gt;        ubyte    rgbBlue&lt;br /&gt;        ubyte    rgbGreen&lt;br /&gt;        ubyte    rgbRed&lt;br /&gt;    }&lt;br /&gt;    .skip (&amp;rgbRed - &amp;startOfRow - 1) % 4&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;You could also insert arbitrary groupings into the format, which would fire an event in the parser that you could use to change state, pretty much like nested elements in an XML file.&lt;br /&gt;&lt;br /&gt;One feature that didn't make it into SPIFF was that old "shipping".  The code sat in my svn repo doing precisely nothing (I never did get back to that project with the ITunesDB either). Spurred on by the realisation that I never actually finish anything, I recently picked up SPIFF again, with the aim of turning it into something more like JAXB, where the parser can populate (or even generate) an annotated object graph.&lt;br /&gt;&lt;br /&gt;Hence, consider this the first in a series of posts covering aspects of the ongoing development of SPIFF, and my attempt to at least get it in the public domain.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-1142794722225106425?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/1142794722225106425/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=1142794722225106425' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/1142794722225106425'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/1142794722225106425'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2010/01/in-re-development-spiff.html' title='In (Re-)Development: SPIFF'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-4540971851491609030</id><published>2009-11-05T12:32:00.000-08:00</published><updated>2009-11-17T14:25:09.341-08:00</updated><title type='text'>Irkonomics</title><content type='html'>Tonight, I have done a bad thing. A very bad thing.  Not bad evil bad.  Or bad naughty bad.  But bad depressing bad.  I fired up my old laptop, my trusty Acer 5003WLMi. The little fella wasn't packing too much punch any more, especially trying to do Eclipse plugin development, and the battery had gone south, so a couple of months ago he was shunted under the sofa in favour of a new shiny Acer 6930G.  Dual core Intel P7450, 4GB RAM, 320GB disk, Nvidia card - the new recruit is a perky number.  Finally I can turn on Compiz effects.  The wireless is rock solid.  I've got more disk that I know what to do with. I certainly got bang for my buck.&lt;br /&gt;&lt;br /&gt;But it has a dark side to it.  It appears that in a bid to save money, Acer have manufactured the 6930G without any human ever laying a hand on it's sorry little shell.  For if they had, they would have found it immediately that the Acer 6930G is probably the most uncomfortable laptop I've ever had the misfortune to use.  For the last two months, I've convinced myself that it's not that bad.  But getting the old 5003WLMi out again has me weeping into my trackpad with it's smooth curves.&lt;br /&gt;&lt;br /&gt;Of course, you don't need melodramatics and hyperbole, so here's the facts:&lt;br /&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;The trackpad.  Oh the trackpad.  The trackpad is so bad it demands... A SUBLIST....&lt;/li&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;It's left of centre. Fail.&lt;/li&gt;&lt;br /&gt;&lt;li&gt;It doesn't have a defined edge.  It's just kind of sunken. Want edge scrolling? Forget it.  Oh, except for there's a little ridge down one part that sort of tells you where the edge scrolling should be. Want tap zones? Really? Not here you don't. Of course, you can &lt;i&gt;have&lt;/i&gt; them, just don't expect to &lt;i&gt;find&lt;/i&gt; them without tapping 10 times.&lt;/li&gt;&lt;br /&gt;&lt;li&gt;The sensitivity is terrible&lt;/li&gt;&lt;br /&gt;&lt;li&gt;It's &lt;i&gt;bumpy&lt;/i&gt;. Yeah, I know, you can't even understand what I'm trying to tell you, right? The plastic on the top panel has little &lt;strike&gt;raised bumps&lt;/strike&gt;dimples, and that texture is also on the trackpad.  I'm a guitar player, so the tips of my fingers aren't exactly Fairy Soft, and yet still I get sore fingers.&lt;/li&gt;&lt;/ul&gt;&lt;br /&gt;&lt;li&gt;The front edge of the laptop is sharp.  You know, that bit where you rest the edge of your hand when you're using the trackpad.  "Hey, you know what this laptop needs?  Sharp bits where you put your hands!  Yeah!"&lt;/li&gt;&lt;br /&gt;&lt;li&gt;The screen only tilts back to 45 degrees.  "Hey you know what users of this laptop need? A sore neck when they're using it standing up! Yeah!"&lt;/li&gt;&lt;br /&gt;&lt;li&gt;My kids could use the keyboard as a trampoline, it's that bouncy&lt;/li&gt;&lt;br /&gt;&lt;li&gt;"Hey, you know what programmers will &lt;i&gt;&lt;b&gt;love&lt;/b&gt;&lt;/i&gt;? The PgUp/PgDown/Home/End keys out the way at the bottom of the number pad!  While you're doing that, why not stick the PgUp key &lt;i&gt;right next to the right arrow key&lt;/i&gt;.  You know, so that you always hit the frickin' thing when you're trying to just move along the line! Yeah!"&lt;/li&gt;&lt;/ul&gt;So, in summary, you want a great performing laptop for not much money? Buy an Acer 6930G.  Just don't ever use another laptop, lest you should remember what you're missing.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-4540971851491609030?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/4540971851491609030/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=4540971851491609030' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/4540971851491609030'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/4540971851491609030'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2009/11/irkonomics.html' title='Irkonomics'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-3476425535325886127</id><published>2009-04-27T05:08:00.000-07:00</published><updated>2009-04-30T05:09:58.322-07:00</updated><title type='text'>Adventures in 3D: Part X - On the move</title><content type='html'>Yay, so we've finally got to the point where we can move a camera around in our scene, thanks to matrices.  In the last post, we used a TransformMatrix to convert the world z-coordinate to something relative to the camera position, albeit with a hard coded view point. Now we just need to start hooking that into something that allows us to change that viewpoint.  The &lt;code&gt;ThreeDeePanel&lt;/code&gt; class already contains a &lt;code&gt;worldToCamera&lt;/code&gt; variable that stores the transform matrix representing the camera view. Let's do things properly now - it's not hard to imagine that a camera is going to have a few different properties, so let's create a Camera class. Let's also represent the position as a Point instead of the raw transform matrix.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public class Camera {&lt;br /&gt;&lt;br /&gt; private Point position;&lt;br /&gt; &lt;br /&gt; public Camera(Point p) {&lt;br /&gt;  position = p;&lt;br /&gt; }&lt;br /&gt; &lt;br /&gt; public Point getPosition() {&lt;br /&gt;  return position; &lt;br /&gt; }&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;Then we keep an instance of the Camera in the ThreeDeePanel class, and provide a method for other classes to get at it.  We also need to calculate the corresponding Matrix from the camera's viewpoint before calling &lt;code&gt;project()&lt;/code&gt;:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;Camera camera = new Camera(new Point(0,0,-300));&lt;br /&gt;&lt;br /&gt;public void getCamera(Point p) {&lt;br /&gt;        return camera;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;protected void paintComponent(Graphics g) {&lt;br /&gt;        ...&lt;br /&gt;        TransformMatrix worldToCameraTransform = TransformMatrix.getWorldToCamera(camera.getPosition());&lt;br /&gt;        for(Primitive poly : renderScene){&lt;br /&gt;  poly.project(worldToCameraTransform);&lt;br /&gt;  poly.draw(g2);&lt;br /&gt; }&lt;br /&gt;        ...&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;In the ThreeDee class, which is doing all the input, I'm going to listen out for the cursor keys (assuming we're in the MOVE_CAMERA state), grab the camera and change it's position:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;private int CAMERA_SPEED = 3;&lt;br /&gt;&lt;br /&gt;public ThreeDee() {&lt;br /&gt;    ...&lt;br /&gt;    panel.addKeyListener(new KeyAdapter() {&lt;br /&gt;        public void keyPressed(KeyEvent e) {&lt;br /&gt;           ....&lt;br /&gt;  } else if (moveState == MoveState.MOVE_CAMERA) {&lt;br /&gt; boolean ctrlDown = ((e.getModifiersEx() &amp; MouseEvent.CTRL_DOWN_MASK) == MouseEvent.CTRL_DOWN_MASK);&lt;br /&gt; Camera camera = panel.getCamera();&lt;br /&gt; switch(e.getKeyCode()) {&lt;br /&gt; case(KeyEvent.VK_UP):&lt;br /&gt;  if(ctrlDown) {&lt;br /&gt;   camera.getPosition().y += CAMERA_SPEED;&lt;br /&gt;  } else {&lt;br /&gt;   camera.getPosition().z += CAMERA_SPEED;&lt;br /&gt;  }&lt;br /&gt;  break;&lt;br /&gt; case(KeyEvent.VK_DOWN):&lt;br /&gt;  if(ctrlDown) {&lt;br /&gt;   camera.getPosition().y -= CAMERA_SPEED;&lt;br /&gt;  } else {&lt;br /&gt;   camera.getPosition().z -= CAMERA_SPEED;&lt;br /&gt;  }&lt;br /&gt;  break;&lt;br /&gt; case(KeyEvent.VK_RIGHT):&lt;br /&gt;  camera.getPosition().x += CAMERA_SPEED;&lt;br /&gt;  break;&lt;br /&gt; case(KeyEvent.VK_LEFT):&lt;br /&gt;  camera.getPosition().x -= CAMERA_SPEED;&lt;br /&gt;  break;&lt;br /&gt; }&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;Give that a go, and you should have a moving camera! However, move around a bit and you'll probably notice a problem.  The &lt;code&gt;canBeCulled()&lt;/code&gt; method in the Triangle class is still using a hard coded viewpoint to decide whether a face is away from the viewer, so if you move up alongside the object, you'll see that it's rear end is missing.  We just need to adjust that method to take the new viewpoint into account (again, don't forget to change the method signature in the Primitive interface):&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public boolean canBeCulled(Point camera) {&lt;br /&gt; if (WIREFRAME) return false;&lt;br /&gt;&lt;br /&gt; Vector viewer = camera.vectorTo(getPosition());&lt;br /&gt; double cull = normal.dotProduct(viewer);&lt;br /&gt; return (cull &gt; 0);&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_U7NJax4HHlo/SfWosVetU1I/AAAAAAAACTU/6XNRoVEW_-Q/s1600-h/camera.png"&gt;&lt;img style="cursor:pointer; cursor:hand;width: 320px; height: 233px;" src="http://2.bp.blogspot.com/_U7NJax4HHlo/SfWosVetU1I/AAAAAAAACTU/6XNRoVEW_-Q/s320/camera.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5329351213696504658" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;As funky as this is, there is the small problem of only being able to look straight ahead.  With the point where the camera is, we also need to store some information about which way it's pointing.  The camera ought to be free to rotate around any of the axes - look left and right, up and down, and roll side to side.  These are commonly know as &lt;span style="font-style:italic;"&gt;yaw&lt;/span&gt;, &lt;span style="font-style:italic;"&gt;pitch&lt;/span&gt; and &lt;span style="font-style:italic;"&gt;roll&lt;/span&gt;.  The good news is that these are simply rotations, which we already know how to deal with.  What's slightly different is that we're no longer rotating around the world origin, but instead treating the camera as the origin.  Also, going right back to the discussion in Part I, the rotation of the objects in the world is opposite to the camera rotation - turning the camera 90 degrees right is like rotating the world 90 degrees left.&lt;br /&gt;&lt;br /&gt;We'll store the rotation of the camera as three angles, alpha, beta and gamma, which represent the rotation around the X, Y and Z axes respectively.  It's worth considering exactly what that means, to save confusion later.  Rotation around the X axis is the looking up and down (pitch) - it's easy to see the "X" and immediately think it ought to be side-to-side motion.  Likewise, rotation around Y is looking side-to-side (yaw), and around Z is roll.  I'll add a &lt;code&gt;rotate()&lt;/code&gt; method to the Camera to change those angles:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public void rotate(double da, double db, double dg) {&lt;br /&gt; alpha += da;&lt;br /&gt; beta += db;&lt;br /&gt; gamma += dg;&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;Now we just need to be able to get the appropriate &lt;code&gt;RotationMatrix&lt;/code&gt; that represents those angles.  Given rotation matrices Rx, Ry and Rz for each of the three axes, the total rotation is simply Rz.(Ry.Rx).  Note that we multiply Rx and Ry first, then multiply by Rz. Why?  Because applying rotations in different orders gives different results.  Imagine you're looking at a point straight ahead. First, look up 45 degrees.  Then look right 45 degrees.  Then roll over 90 degrees.  You're now looking at a point "top right" of where you were originally, and with your head tilted to one side.  Start again, but this time do the "roll" first.  You'll actually end up (if you've done it properly...) looking at a point "top left" of where you were. The secret is in the fact that "up" and "right" are relative to which way you're already facing.  If you're on your feet and tilt your head backwards to look up, it really is "up".  If you were lying on your side on the ground and tilted your head back, you'd actually still be looking along the ground.  You would actually have to look "right" to look "up".&lt;br /&gt;&lt;br /&gt;So, I'll add a new method to RotationMatrix to get an instance that represents alpha,beta,gamma, which we'll do by getting instances for each axis and multiplying them together.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public static RotationMatrix getInstance(double alpha, double beta, double gamma) {&lt;br /&gt; return RotationMatrix.getInstance(gamma, RotationAxis.Z).times&lt;br /&gt;  (RotationMatrix.getInstance(beta, RotationAxis.Y).times&lt;br /&gt;  (RotationMatrix.getInstance(alpha, RotationAxis.X)));&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;Finally, we implement a &lt;code&gt;getCameraRotation()&lt;/code&gt; method on Camera to get that matrix, and multiply it by the position transform matrix we created above to give the final transform from world to camera.  Note that the method signature for project changes to take a base Matrix type.  Also, we need to change our RotationMatrix class (and any code that calls it) to use homogenous coordinates, so that we can multiply a RotationMatrix and TransformMatrix.  We know that this is pretty simple, just add a row and column to each matrix, with a 1 in the bottom right corner (in hindsight, there's no need for RotationMatrix and TransformMatrix to be different types, really we should just use the base Matrix class. Ah well, you live and learn)&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public RotationMatrix getCameraTransform() {&lt;br /&gt;        Matrix translation = TransformMatrix.getWorldToCamera(position);&lt;br /&gt; return RotationMatrix.getInstance(alpha, beta, gamma).times(translation);&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;and in ThreeDeePanel:&lt;br /&gt;&lt;pre&gt;Matrix worldToCameraTransform = camera.getCameraTransform();&lt;/pre&gt;&lt;br /&gt;This is all very well, but it doesn't actually do much yet.  All we have to do though, is wire it up to the input listeners.  Whilst in the MOVE_CAMERA state, dragging the mouse will look left,right,up,down, and moving the scroll wheel will roll.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public void mouseDragged(MouseEvent e) {&lt;br /&gt;        ...&lt;br /&gt;        case MOVE_CAMERA:&lt;br /&gt;  panel.getCamera().rotate(dy * RADIAN,dx * RADIAN,0.0);&lt;br /&gt; }&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;and the same sort of thing in &lt;code&gt;mouseWheelMoved()&lt;/code&gt; for the roll.  Note that dx (moving the mouse left and right) affects the Y axis rotation, and vice versa, for the reasons we discussed above.&lt;br /&gt;&lt;br /&gt;Run that, and you too can look around the back of your object!  Take care to check whether the rotation is what you expect, as it's not always obvious.  Matrix multiplication is not commutative - AB is not the same as BA - and if you've got something in the wrong order, it'll generally manifest itself here as the axis of rotation being wrong.  For instance, you may find that instead of the object rotating around the camera, the camera rotates around the object. &lt;br /&gt;&lt;br /&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_U7NJax4HHlo/Sfi2XaSJC1I/AAAAAAAACTc/Zr5XEL8_C6A/s1600-h/camera.png"&gt;&lt;img style="cursor:pointer; cursor:hand;width: 320px; height: 240px;" src="http://1.bp.blogspot.com/_U7NJax4HHlo/Sfi2XaSJC1I/AAAAAAAACTc/Zr5XEL8_C6A/s320/camera.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5330210672301640530" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;You may also notice that the scene "stretches" when it's rotated towards the edge of the screen. This is related to the focal length. If the effect looks unnatural, try increasing the focal length - a shorter focal length effectively gives you more of a "fish-eye" look.&lt;br /&gt;&lt;br /&gt;Now you've managed to get around the back, you've probably also noticed a few problems with the z-order.  Remember that we're sorting, and therefore drawing, the screen by (world) z position, which is fine as long as we're looking in the +ve z direction.  As soon as we move elsewhere, that becomes useless - we now need to sort by z order relative to the camera.  That means we need to be converting from world space to camera space before doing the sorting.&lt;br /&gt;&lt;br /&gt;Let's also take the opportunity to tidy up the Triangle class.  We're currently marshalling vertex data between arrays and matrices, whereas we could just keep the vertex data as a matrix and be done with it.  For convenience, I'll add methods to the Vector and Point classes to convert to/from matrices.  &lt;br /&gt;&lt;br /&gt;As well as storing the world coordinates, we'll also keep a matrix of the view coordinates in &lt;code&gt;Triangle&lt;/code&gt;.  These will be populated in the &lt;code&gt;project()&lt;/code&gt; method, which now needs to move in the pipeline to a point before we do the z-order sorting.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public class Triangle extends Primitive {&lt;br /&gt;        // vertices of the triangle in world space&lt;br /&gt; Matrix[] vertices = new Matrix[3];&lt;br /&gt; Matrix[] viewVertices = new Matrix[3];&lt;br /&gt;      &lt;br /&gt;        public void project(Matrix worldToCamera) {&lt;br /&gt;  &lt;br /&gt;  for (int i = 0; i &lt; 3; i++) {&lt;br /&gt;   viewVertices[i] = persMatrix.times(worldToCamera.times(vertices[i]));&lt;br /&gt;&lt;br /&gt;   if(viewVertices[i].data[2][0] &lt; 0) {&lt;br /&gt;    draw = false;&lt;br /&gt;    return;&lt;br /&gt;   }&lt;br /&gt;   &lt;br /&gt;   xPoints[i] = (int) (viewVertices[i].data[0][0] / viewVertices[i].data[3][0]);&lt;br /&gt;   yPoints[i] = (int) (viewVertices[i].data[1][0] / viewVertices[i].data[3][0]);&lt;br /&gt;  }&lt;br /&gt; }&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;The &lt;code&gt;doPipeline()&lt;/code&gt; method now consists of &lt;code&gt;backfaceCulling()&lt;/code&gt;, &lt;code&gt;project()&lt;/code&gt;, &lt;code&gt;sortForZOrder()&lt;/code&gt;, &lt;code&gt;lightScene()&lt;/code&gt;.  All we need to do now is change &lt;code&gt;getZOrder()&lt;/code&gt; in Triangle to return the z order from the (view coords) &lt;code&gt;viewVertices&lt;/code&gt; array instead of the (world coords) &lt;code&gt;vertices&lt;/code&gt; array.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public double getZOrder() {&lt;br /&gt; return getAverage(viewVertices, 2);&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;@Override&lt;br /&gt;public Point getPosition() {&lt;br /&gt; return new Point(getAverage(vertices, 0), getAverage(vertices, 1), getAverage(vertices, 2));&lt;br /&gt;}&lt;br /&gt; &lt;br /&gt;private double getAverage(Matrix[] matrices, int index){&lt;br /&gt; return (matrices[0].data[index][0] + matrices[1].data[index][0] + matrices[2].data[index][0])/3; &lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;There, all sorted.  We're gradually getting there.  Unfortunately, it seems that for every bit we add, it throws up another couple of problems to solve.  But we love solving problems, right? Right?!  Put the kettle on, make a cup of tea, download the source, and let's ponder.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-3476425535325886127?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/3476425535325886127/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=3476425535325886127' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/3476425535325886127'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/3476425535325886127'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2009/04/adventures-in-3d-part-x-on-move.html' title='Adventures in 3D: Part X - On the move'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/_U7NJax4HHlo/SfWosVetU1I/AAAAAAAACTU/6XNRoVEW_-Q/s72-c/camera.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-4031595131270058217</id><published>2009-04-21T05:18:00.000-07:00</published><updated>2009-04-26T08:24:21.737-07:00</updated><title type='text'>Adventures in 3D: Part IX - A Bit Of Perspective</title><content type='html'>Stick with it 3D fans, we're getting there.&lt;br /&gt;&lt;br /&gt;One thing we cheated at way back in &lt;a href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-i-basics.html"&gt;Part I&lt;/a&gt; was perspective.  Until now, everything has been drawn using parallel projection.  That is, there is no perspective, everything appears the same size regardless of how far away it is.  That works fine when you're looking at a single object, where the difference in distance between the front and the back of the object is small enough to be negligible in terms of how your brain perceives the image, but when you start adding objects into the scene in the background, it's a problem.&lt;br /&gt;&lt;br /&gt;Thankfully, perspective is very simple to do.  We're actually going to do this twice. For a first pass, we'll do the simplest possible thing, which is to just do the calculations explicitly, and with a hardcoded viewpoint.  Hopefully your alarm bells will be ringing at the sight of the work "hardcode", so then we'll look at the more proper solution, which involves our old friend, the matrix.&lt;br /&gt;&lt;br /&gt;So, first solution.  We already have a &lt;code&gt;project()&lt;/code&gt; method in the Triangle class, which is used to convert the 3D model's x,y,z (double) coordinates into the screen's x,y (integer) coordinates. Remember that perspective does not affect the 3D model in any way, everything stays where it is.  Perspective is simply an effect of projection, so this is exactly where we need to be doing the perspective calculations.  And what does "perspective" actually mean for our projection?  Think about a wireframe cube rendered in 3D with perspective.  The back face of the box, which is at a greater Z distance, will appear smaller than the front face - the left and right sides of the back face have smaller x values (assume the x and y axes are through the centre of the box), and the top and bottom sides of the back face have smaller y values.  So it's clearly an adjustment of X and Y coordinates as a function of Z, we just need to figure out what that adjustment is.  Time for a diagram.&lt;br /&gt;&lt;br /&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_U7NJax4HHlo/Se4n-qi0WJI/AAAAAAAACTM/N24X_2ezRMc/s1600-h/perspective.png"&gt;&lt;img style="cursor: pointer; width: 320px; height: 125px;" src="http://3.bp.blogspot.com/_U7NJax4HHlo/Se4n-qi0WJI/AAAAAAAACTM/N24X_2ezRMc/s320/perspective.png" alt="" id="BLOGGER_PHOTO_ID_5327239366751574162" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;The vertical line in the middle represents the screen onto which the model is projected - the camera C is at some distance z' behind that screen, and the point P is at a distance z beyond that screen, and distance y above the axis. Drawing a line from C to P represents our line of sight, and you can see that it intersects the screen at a distance y' above the axis. Our job is to figure out the distance y'.  Cast your mind back to school maths classes, and the idea of &lt;a href="http://www.mathsrevision.net/gcse/pages.php?page=38"&gt;similar triangles&lt;/a&gt;.  The theory of similar triangles says that two triangles with the same angles will have sides that are in proportion. Therefore, y'/z' = y/(z+z'), which rearranged slightly gives the equation&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;           z'&lt;br /&gt;y' = y * -----&lt;br /&gt;         z + z'&lt;/pre&gt;&lt;br /&gt;From that, you can see that as z tends towards zero (i.e. the object moves nearer the plane of projection), the second term becomes z'/z', which is 1, and so y'=y. Working out z is fairly easy, we just need to remember that it's the distance from the screen to the object, which, if we decide the screen is somewhere other than z=0, is not the same as the z coordinate in the world space.  In other words, z = z&lt;span style="font-size:50%;"&gt;world&lt;/span&gt; - z&lt;span style="font-size:50%;"&gt;screen&lt;/span&gt; .  We also need z' - this is actually a fairly abitrary number, representing the focal length.  The lower this number, the more pronounced the perspective effect.&lt;br /&gt;&lt;br /&gt;So, let's stick that into some code. We define a viewpoint that represents the position of the viewer, and a focal length (z' from the diagram), in this case determined pretty much by trial and error - this value gives a decent sense of depth without looking unrealistic.  As the focal length is fixed, and the viewpoint will potentially move, we calculate the position of our "screen" as being the position of the camera plus the focal length.  Then, the relative z distance is calculated (z in the diagram), being the distance from the screen to the object.  Finally, we use those values to calculate the perspective correction as defined above. &lt;br /&gt;&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;private Point viewpoint = new Point(0,0,-300);&lt;br /&gt;double focalLength = 300;&lt;br /&gt;&lt;br /&gt;public void project() {&lt;br /&gt;        double zScreen = viewpoint.z + focalLength;&lt;br /&gt; for (int i = 0; i &lt; 3; i++) {&lt;br /&gt;  double zDistance = z[i] - zScreen;&lt;br /&gt;  double perspective = focalLength / (focalLength + zDistance);&lt;br /&gt;  xPoints[i] = (int) (x[i] * perspective);&lt;br /&gt;  yPoints[i] = (int) (y[i] * perspective);&lt;br /&gt; }&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;If you spin the scene objects now, you should get some sense of perspective.  If you can't really see anything, you may want to lower the focal length value so the effect is more pronounced.  &lt;br /&gt;&lt;br /&gt;That's all well and good, but there's another way to achieve the same effect, and it's going to set us up a bit better for getting the camera moving around.  We're going to use a matrix to perform the same sort of maths.&lt;br /&gt;&lt;br /&gt;Last time we used a matrix it was for rotation, and was a 3x3 matrix which acted on 1x3 matrix (the point).  Now we're going to use a transformation matrix to translate points, both in 3D and 2D. Recall that if we're working with a 3x3 matrix on a point x,y,z, then matrix multiplication means the output for each coordinate is of the form Ax + By + Cz.  However, in the case of translation, we often need to just add or subtract a constant that is not a function of position.  This may sound familiar, for this is the definition of an &lt;a href="http://en.wikipedia.org/wiki/Affine_transformation"&gt;affine transformation&lt;/a&gt;, which we've already been happily using to move the origin into the centre of the screen.  To do affine transforms, we need to introduce &lt;a href="http://en.wikipedia.org/wiki/Homogenous_coordinates"&gt;homogenous coordinates&lt;/a&gt;.  There is, I'm sure, lots of complicated geometry mathematics that can be used to describe homogenous coordinates - see that Wikipedia page for starters - but really you can just think of it as a hack to allow transformations of the form Ax + By + Cz + D.  You do two things: add a column to the translation matrix containing the constants to add to each coordinate, and add a 4th row, with the value, 1 to the vector matrix.  Easy.  Here's an example:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;|1 0 0 30 ||x|    |x + 30|&lt;br /&gt;|0 1 0 10 ||y|    |y + 10|&lt;br /&gt;|0 0 1 -10||z| =&gt; |z - 10|&lt;br /&gt;|0 0 0  1 ||1|    |  1   |&lt;/pre&gt;&lt;br /&gt;Hopefully you can see how this can start getting us towards the idea of a moveable camera.  The translation coordinates in the 4th column will come from the position of the camera, and the result will be to move the world coordinates to coordinates relative to the camera.  We did the same thing in our first method in calculating (focalLength + zDistance), albeit only for the z axis.  You can see that with the matrix method, we can very easily take the x and y coordinates into account as well.&lt;br /&gt;&lt;br /&gt;Let's add some code.  I create a new TransformationMatrix class, and simply have a static method &lt;code&gt;worldToCamera(Point view)&lt;/code&gt; that, given a camera position, will return a matrix of the form:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;|1 0 0 -view.x|&lt;br /&gt;|0 1 0 -view.y|&lt;br /&gt;|0 0 1 -view.z|&lt;br /&gt;|0 0 0    1   |&lt;/pre&gt;&lt;br /&gt;That code is&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public class TransformMatrix extends Matrix {&lt;br /&gt;&lt;br /&gt;    private TransformMatrix(double[][] data) {&lt;br /&gt; super(data);&lt;br /&gt;    }&lt;br /&gt; &lt;br /&gt;    public static TransformMatrix getWorldToCamera(Point view) {&lt;br /&gt; return new TransformMatrix(new double[][] { {1,0,0,-view.x}, {0,1,0,-view.y}, {0,0,1,-view.z}, {0,0,0,1}});&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Note that the view coordinates are negated.  If the camera is at z=10, and a world point is at z=20, the point will be 10 units from the camera i.e. z = z&lt;span style="font-size:50%;"&gt;world&lt;/span&gt; - z&lt;span style="font-size:50%;"&gt;camera&lt;/span&gt;. We'll pass in a matrix to the &lt;code&gt;project()&lt;/code&gt; method to use for the transform from world to camera (don't forget to make that change in the &lt;code&gt;Primitive&lt;/code&gt; interface too).  For now you can just pick a camera position and hard code it in the call to &lt;code&gt;project()&lt;/code&gt;.  When we get round to moving the camera, that matrix will be recalculated each time.&lt;br /&gt;&lt;br /&gt;So how does this help us with perspective?  It doesn't yet. We also need to factor in that focalLength.  In our world-to-camera transform, we're going to end up with a z-coordinate, z',  that is the distance from the camera to the point.  In the first effort above, we had zDistance, which was the distance from the screen to the point, and focalLength which was the distance from camera to screen.  That means that:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;z' = focalLength + zDistance&lt;/pre&gt;&lt;br /&gt;How very handy. The perspective calculation is now:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;double perspective = focalLength / z';&lt;/pre&gt;&lt;br /&gt;We can express that in a matrix multiplication as well. The trick here is to use the homogenous coordinate (normally called &lt;i&gt;&lt;b&gt;w&lt;/b&gt;&lt;/i&gt;) to store that perspective calculation (&lt;i&gt;&lt;b&gt;w'&lt;/b&gt;&lt;/i&gt;) and then it's a simple case of applying that to x' and y'.  Just one other thing we have to think about - as we're multiplying matrices, we need to express the perspective as a multiplication of z' rather than dividing by it, so we simply turn it upside down, and instead of multiplying x' by w', we divide.&lt;br /&gt;&lt;br /&gt;That means the perspective calculation can be applied as a matrix, although in our simple case it's nothing more than a way of dividing z' by the focal length. The benefit of using the matrix is that you could potentially encode other operations in there in future to apply different effects.  Here's what the matrix looks like, and the result of applying that to homogenous coordinates:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;| 1  0  0  0|| x' |    |  x'  |&lt;br /&gt;| 0  1  0  0|| y' |    |  y'  |&lt;br /&gt;| 0  0  1  0|| z' | =&gt; |  z'  |&lt;br /&gt;| 0  0 1/f 0|| w' |    | z'/f | =&gt; &lt;b&gt;&lt;i&gt;w&lt;span style="font-size:50%;"&gt;p&lt;/span&gt;&lt;/i&gt;&lt;/b&gt;&lt;/pre&gt;&lt;br /&gt;Let's recap:  &lt;br /&gt;&lt;ul&gt;&lt;li&gt;Given a point &lt;b&gt;&lt;i&gt;x,y,z&lt;/i&gt;&lt;/b&gt;, we add the homogenous coordinate (which is just a 1) to give a vector matrix &lt;b&gt;&lt;i&gt;x,y,z,w&lt;/i&gt;&lt;/b&gt;.&lt;/li&gt;&lt;br /&gt;&lt;li&gt;The world-to-camera transform matrix is applied to give the coordinates &lt;b&gt;&lt;i&gt;x',y',z',w'&lt;/i&gt;&lt;/b&gt;, which are the coordinates of the point relative to the camera position, and where &lt;b&gt;&lt;i&gt;w'&lt;/i&gt;&lt;/b&gt; is still just a 1.&lt;/li&gt;&lt;br /&gt;&lt;li&gt;The perspective matrix is applied to calculate &lt;b&gt;&lt;i&gt;w&lt;span style="font-size:50%;"&gt;p&lt;/span&gt;&lt;/i&gt;&lt;/b&gt;, which is the perspective correction factor&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Divide &lt;b&gt;&lt;i&gt;x'&lt;/i&gt;&lt;/b&gt; and &lt;b&gt;&lt;i&gt;y'&lt;/i&gt;&lt;/b&gt; by &lt;b&gt;&lt;i&gt;w&lt;span style="font-size:50%;"&gt;p&lt;/span&gt;&lt;/i&gt;&lt;/b&gt; to give the final x and y coordinates&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;Sounds slight complicated, but it's really not doing anything more than we've already done.  Again, the benefit is in being able to encode other transformations in the matrices, which should come in useful shortly.&lt;br /&gt;&lt;br /&gt;In code, it's straightforward. We'll add a new method &lt;code&gt;getPerspective(double focalLength)&lt;/code&gt; to the TransformMatrix class to return a matrix that divides z' by the focalLength:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public static TransformMatrix getPerspective(double focalLength) {&lt;br /&gt; return new TransformMatrix(new double[][] { {1,0,0,0}, {0,1,0,0}, {0,0,1,0}, {0,0,1/focalLength, 0} });&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;Then in the Triangle class:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public void project(TransformMatrix worldToCamera) {&lt;br /&gt;  &lt;br /&gt; for (int i = 0; i &lt; 3; i++) {&lt;br /&gt;  Matrix point = new Matrix(new double[][] { {x[i]}, {y[i]}, {z[i]}, {1}});&lt;br /&gt;  Matrix result = worldToCamera.times(point);&lt;br /&gt;   &lt;br /&gt;  Matrix finalPoints = TransformMatrix.getPerspective(FOCALLENGTH).times(result);&lt;br /&gt;   &lt;br /&gt;  xPoints[i] = (int) (finalPoints.get(0,0) / finalPoints.get(3,0));&lt;br /&gt;  yPoints[i] = (int) (finalPoints.get(1,0) / finalPoints.get(3,0));&lt;br /&gt; }&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;Of course, &lt;code&gt;result&lt;/code&gt; is just an intermediate, and the perspective matrix never changes given a fixed focal length, so if you're the sort of coder who hates to see waste, you can store the perspective matrix in the Triangle class, and do the whole lot in one go:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;private final TransformMatrix persMatrix = TransformMatrix.getPerspective(focalLength);&lt;br /&gt;&lt;br /&gt;public void project(TransformMatrix worldToCamera) {&lt;br /&gt;        ...&lt;br /&gt;       Matrix finalPoints = persMatrix.times(worldToCamera.times(point));&lt;br /&gt;        ...&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;a href="http://revbingo.f2s.com/blogfiles/PartIX.zip"&gt;Download the source&lt;/a&gt; and see for yourself.&lt;br /&gt;&lt;br /&gt;One final thing for this episode - I promise. If you move your camera to a position that means objects going behind the camera, you'll see things go a bit pear-shaped because we're trying to render objects that should not be in the view.  So there needs to be some sort of check to ensure polygons that are behind the camera are not drawn.  That's easy enough, any object which has a negative z' (remember, z' is relative to the camera) should not be drawn. This is slightly tricky, because we need to tell the &lt;code&gt;draw()&lt;/code&gt; method that. I'm going to hack it for now, and use an instance variable &lt;code&gt;boolean draw = true;&lt;/code&gt;. So then in &lt;code&gt;project()&lt;/code&gt;, we do the check:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;if(finalPoints.get(2,0) &lt; 0) {&lt;br /&gt; draw = false;&lt;br /&gt; return;&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;and in &lt;code&gt;draw()&lt;/code&gt;:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public void draw(Graphics2D graphics) {&lt;br /&gt;    if(draw == false) {&lt;br /&gt;     draw = true;&lt;br /&gt;     return;&lt;br /&gt;    }&lt;br /&gt;    ...&lt;br /&gt;}&lt;/pre&gt; &lt;br /&gt;Note that we reset the draw variable once we've decided not to draw the polygon, so that it can be considered for drawing again in the next frame.&lt;br /&gt;&lt;br /&gt;That's a fair slice of stuff for what was really a quite simple bit of functionality.  Next time we'll start getting that camera moving, and also think a bit more seriously about that last point.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-4031595131270058217?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/4031595131270058217/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=4031595131270058217' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/4031595131270058217'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/4031595131270058217'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2009/04/adventures-in-3d-part-ix-bit-of.html' title='Adventures in 3D: Part IX - A Bit Of Perspective'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/_U7NJax4HHlo/Se4n-qi0WJI/AAAAAAAACTM/N24X_2ezRMc/s72-c/perspective.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-7071180727511256004</id><published>2009-04-19T11:53:00.000-07:00</published><updated>2009-04-20T13:46:52.217-07:00</updated><title type='text'>Adventures in 3D: Part VIII - A Light Touch</title><content type='html'>&lt;i&gt;(Yet again, I'm deviating from the original aim of getting a camera persepective working. But this is pretty cool, so hopefully you'll forgive)&lt;/i&gt;&lt;br /&gt;&lt;br /&gt;First, a confessional. There was a pretty fundamental error in the Point class, in that vectorTo() was implemented such that vectors were actually backwards.  D'oh.  Which explained why, when I actually took the time to think about where lights were coming from and how the scene was lit, things were the wrong way round.  That's fixed now, along with a couple of other things that were also wrong in compensation for that error.  At least it's a good lesson in taking the time to properly consider such fundamentals, rather than ploughing on with whatever works... On the positive side, all the concepts introduced thus far still stand.&lt;br /&gt;&lt;br /&gt;Anyway, so far, the lighting on this object has been pretty dumb.  The light we've modelled is just a Vector, so any object anywhere in the scene is lit from the same direction and at the same intensity, and it's also pretty boring white light.  We're going to spice things up a bit.  In a 3D scene, there can be various types of lighting with different characteristics of where and how the light is cast.  In our case, we're going to implement two types of light - ambient light and spotlights.&lt;br /&gt;&lt;br /&gt;Ambient lights are super super easy.  They're just a background level of light that's present everywhere.  It doesn't come from a point, doesn't point in any particular direction, doesn't change in intensity.  Think of it like daylight on a cloudy day - the light is just kind of there, without coming from any particular place.&lt;br /&gt;&lt;br /&gt;To make life easier, we have an abstract Light superclass. This says that a light usually has a colour, a position and a direction, although, in the case of an ambient light, we just ignore those last two. What the Light class doesn't define is how the light affects the surfaces it falls on.  For that, we have an abstract &lt;code&gt;light(Lightable s)&lt;/code&gt; method, which returns a Color, being the colour (remember that brightness is one component of a colour) that this light contributes to that surface.  The Lightable interface defines two methods, getNormal() and getPosition() - any object (in our case, a Primitive) that wants to be lit must implement these two methods so that the lights can tell where the surface is and which way it's facing.  You can easily see that this interface could need to define other methods in the future for more sophisticated lighting - for instance, the light() method may need to know the absorptive or reflective properties of a surface.&lt;br /&gt;&lt;br /&gt;The AmbientLight class only holds one thing - the colour of the light.  The implementation of the light() method is dead simple, because the light that the AmbientLight contributes to each surface is simply it's colour.  No need to worry about which way the surface is facing or how far away it is.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public class AmbientLight extends Light {&lt;br /&gt;&lt;br /&gt; public AmbientLight(Color color) {&lt;br /&gt;  this.color = color;&lt;br /&gt; }&lt;br /&gt;&lt;br /&gt; @Override&lt;br /&gt; public Color light(Lightable s) {&lt;br /&gt;  return this.color;&lt;br /&gt; }&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;Our Triangle class has a lightPolygon() method, which is where we ask all the lights in the scene to tell us what they will contribute to the final colour of this polygon. It's just a loop calling light(this) for each light.  The colour from each light is added together to get a final colour to render.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public void lightPolygon(LightScene lights) {&lt;br /&gt; litColor = Color.black;&lt;br /&gt; for(Light light : lights) {&lt;br /&gt;   litColor = addColor(litColor, light.light(this));&lt;br /&gt; }&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;The addColor() method is also very simple - just add each RGB component separately, naturally making sure that the component values don't go above 255.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;private Color addColor(Color c1, Color c2) {&lt;br /&gt; return new Color(Math.min(255, c1.getRed() + c2.getRed()),&lt;br /&gt;   Math.min(255, c1.getGreen() + c2.getGreen()),&lt;br /&gt;   Math.min(255, c1.getBlue() + c2.getBlue()));&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;Put all that together, and define an AmbientLight with a muted colour.  You don't want the ambient light to be too bright, or else it will just wash out all the other colours in the scene. I'm going to use RGB(0,0,30). The effect this has is to show up all the objects in the scene in a dark blue base light.  If nothing else, it's handy for making sure that your objects are being rendered, where previously they would not have been painted if you got the lighting coordinates wrong.&lt;br /&gt;&lt;br /&gt;Now let's try something far more interesting, the spotlight.  Spotlights have a number of properties - the position of the light, the direction the light points, what colour it is, and the angle that the light spreads out at.  For a more realistic representation, we also want to define how quickly the light falls off from full intensity around the edge.  The first three are already taken care of in our Light superclass.  The second two will be implemented in the Spotlight class, and I'll call them &lt;code&gt;fullIntensityAngle&lt;/code&gt; and &lt;code&gt;falloffAngle&lt;/code&gt;.  For a light defined with a fullIntensityAngle of 20 and falloffAngle of 15, that means that surfaces within 20 degrees of the centre line of the light will be lit at full intensity, and surfaces another 15 degrees beyond that will be lit at an intensity proportional to their distance from the centre line.  At 35 degrees from the centre and beyond, there's no light contributed from the spotlight.&lt;br /&gt;&lt;br /&gt;There are two main calculations to do.  The first is the standard calculation we're used to, work out which way the surface faces, and if it's facing away from the light, just return Color.BLACK (as far as adding lights is concerned, Color.BLACK is a null result).&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;double dtFace = s.getNormal().normalise().dotProduct(lightNormal);&lt;br /&gt;if(dtFace &gt;= 0) return Color.BLACK;&lt;/pre&gt;&lt;br /&gt;where &lt;code&gt;lightNormal&lt;/code&gt; is the normalised vector pointing in the direction of the light.&lt;br /&gt;&lt;br /&gt;Next, get a vector from the light to the surface, normalise it and calculate the dot product with &lt;code&gt;lightNormal&lt;/code&gt;.  For vectors of unit length, the dot product of the two gives the cosine of the angle between the two.  At this point, we could use &lt;code&gt;Math.acos()&lt;/code&gt; to convert back to an angle and figure out if it's within the spread of our light.  But acos is a pretty expensive operation, so instead of comparing angles, we just compare raw cosine values (the cosine of the spread angle is calculated in the constructor or when the angles are changed) to see if the surface is outside the range. If it is, again, return Color.BLACK.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;Point lightSource = this.getPosition();&lt;br /&gt;Vector lightToPoly = lightSource.vectorTo(s.getPosition());&lt;br /&gt;double dtPosition = lightToPoly.normalise().dotProduct(lightNormal);&lt;br /&gt;if(dtPosition &amp;lt; cosFullSpread) return Color.BLACK;&lt;/pre&gt;&lt;br /&gt;Ok, now we're down to just the points that are actually lit. At this point, we will do that acos() operation to get the angle. This makes things simple as it's a straight comparison of angles to determine how to light the surface, and is also important because it means that when we calculate the falloff, it's linear with angle, rather than the cosine.&lt;br /&gt;&lt;br /&gt;Within the spread of the fullIntensityAngle, the surfaces are light at the brightness determined by the direction they face, as usual. In the "fall off zone", the intensity of the light dims the further away you get from the centre, so we calculate a falloffFactor, which is a number from 0.0 to 1.0, by which we'll multiply the brightness in the final colour.  Note that in the final colour, we create a HSB colour, which has the same hue and saturation as the specified light colour, and just scale the brightness.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;double angle = Math.acos(dtPosition) * (180/Math.PI);&lt;br /&gt;&lt;br /&gt;double fullSpreadAngle = fullIntensityAngle + falloffAngle;&lt;br /&gt;double falloffFactor = 1;&lt;br /&gt;if (angle &amp;gt;= fullIntensityAngle &amp;amp;&amp;amp; angle &amp;lt;= fullSpreadAngle) {&lt;br /&gt;   falloffFactor -= ((angle - fullIntensityAngle)/(falloffAngle)); &lt;br /&gt;}  &lt;br /&gt;litColor = Color.getHSBColor(colorHue, &lt;br /&gt;                      colorSaturation, &lt;br /&gt;                   (float) (Math.abs(dtFace) * falloffFactor * colorBrightness)); &lt;br /&gt;return litColor;&lt;/pre&gt;&lt;br /&gt;Throw all this together, sprinkle a few different colour lights around, use a bit of artistic licence to add some other code (see below), and what do you get?&lt;br /&gt;&lt;br /&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_U7NJax4HHlo/SeuUnytVTbI/AAAAAAAACS8/NaHUMqDvDu0/s1600-h/Colours.png"&gt;&lt;img style="cursor: pointer; width: 320px; height: 240px;" src="http://2.bp.blogspot.com/_U7NJax4HHlo/SeuUnytVTbI/AAAAAAAACS8/NaHUMqDvDu0/s320/Colours.png" alt="" id="BLOGGER_PHOTO_ID_5326514395643465138" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;There is no denying that's pretty damn sexy.  We can also do one more thing to bring colour to the scene, and that's to give the polygons themselves some colour. We'll assign a base colour to each polygon, and adjust the final lit colour to account for the surface colour.  That adjustment is not immediately obvious, but if you consider a few cases it becomes apparent, especially if you think about the colour components as floats 0.0-1.0 instead of the traditional integer 0-255.  For instance, a pure white surface (1.0,1.0,1.0) lit by a pure red light (1.0,0.0,0.0) will appear pure red (1.0,0.0,0.0).  A pure red surface lit by a pure blue light (0.0,0.0,1.0) will appear black (0.0,0.0,0.0) - a red surface absorbs all blue wavelengths.  A black surface always appears black, even if lit with white light.  If we write those out, it should become clear:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;(1,1,1) lit by (1,0,0) = (1,0,0)&lt;br /&gt;(1,0,0) lit by (0,0,1) = (0,0,0)&lt;br /&gt;(0,0,0) lit by (x,y,z) = (0,0,0)&lt;/pre&gt;&lt;br /&gt;It is, of course, multiplication of the colour components.  A quick multiplyColour method:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;private Color multiplyColor(Color c1, Color c2) {&lt;br /&gt;        float[] c1Comp = c1.getColorComponents(null);&lt;br /&gt; float[] c2Comp = c2.getColorComponents(null);&lt;br /&gt; return new Color(c1Comp[0] * c2Comp[0], &lt;br /&gt;   c1Comp[1] * c2Comp[1],&lt;br /&gt;   c1Comp[2] * c2Comp[2]);&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;and then apply that to the lit colour:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;litColor = multiplyColor(litColor, surfaceColor);&lt;/pre&gt;&lt;br /&gt;and then you have coloured polygons:&lt;br /&gt;&lt;br /&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_U7NJax4HHlo/SezeMI1idaI/AAAAAAAACTE/am-KRdgE7K0/s1600-h/Colours2+.png"&gt;&lt;img style="cursor:pointer; cursor:hand;width: 320px; height: 240px;" src="http://1.bp.blogspot.com/_U7NJax4HHlo/SezeMI1idaI/AAAAAAAACTE/am-KRdgE7K0/s320/Colours2+.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5326876759384290722" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;As &lt;a href="http://en.wikipedia.org/wiki/Shed_Seven"&gt;Shed Seven&lt;/a&gt; once sang, it's getting better all the time.&lt;br /&gt;&lt;br /&gt;Cut out the middle man and just &lt;a href="http://revbingo.f2s.com/blogfiles/PartVIII.zip"&gt;download the source&lt;/a&gt;.  Not least because there's plenty of other tinkering I've done with the code.  Of most interest:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;There's a new BasicSceneObject, XYPlane, which provides the "back wall" effect. Notice that the rotate() method is overriden, with no implementation, which means it stays static whilst the other objects in the scene rotate in front of it.&lt;/li&gt;&lt;li&gt;The pipeline was previously using ArrayLists to store the list of polygons.  The problem with this is that the backface culling does a remove() on the list, which is not very efficient for ArrayLists, because they then have to shuffle other objects in the list down the backing array.  By changing to a LinkedList, for which removals are O(1) (simply change pointers), performance is improved.&lt;/li&gt;&lt;li&gt;For some sexy debugging, the InfoPanel class allows us to draw some basic info in the top left of the panel&lt;/li&gt;&lt;li&gt;Now we've got spotlights in the scene, it's useful to be able to move them around.  There are some extra controls that you can use to control the scene:&lt;/li&gt;&lt;li&gt;Space cycles through modes of 1) rotating objects, 2) moving the focus point of the current light, 3) moving the position of the current light, 4) moving the camera (wait, not yet!). &lt;/li&gt;&lt;li&gt;In MOVE_OBJECT mode, clicking and dragging rotates X and Y axes.  Using the scroll wheel (or edge drag on a touchpad) rotates the axis&lt;br /&gt;&lt;/li&gt;&lt;li&gt;In MOVE_LIGHT_POSITION mode, clicking and dragging moves the light source position in XY.  Holding CTRL whilst doing so moves the light backwards and forwards.&lt;/li&gt;&lt;li&gt;In MOVE_LIGHT_DIRECTION mode, clicking and dragging moves the focus point of the light in XY.  Holding CTRL whilst doing so moves the focus point backwards and forwards.   Using the scroll wheel changes the size of the falloffAngle of the light, and holding CTRL while scrolling changes the fullIntensityAngle.&lt;/li&gt;&lt;li&gt;In both MOVE_LIGHT_POSITION and MOVE_LIGHT_DIRECTION, pressing N will cycle control through available spotlights (Warning: this is a bit of hackery - if you don't have a spotlight in the scene, this will go into an infinite loop...)&lt;/li&gt;&lt;li&gt;In all modes, clicking the mouse will toggle between wireframe and full mode.&lt;/li&gt;&lt;/ul&gt;That's a decent slab of work.  A simple one for next time - adding some perspective.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-7071180727511256004?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/7071180727511256004/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=7071180727511256004' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/7071180727511256004'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/7071180727511256004'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2009/04/adventures-in-3d-part-viii-light-touch.html' title='Adventures in 3D: Part VIII - A Light Touch'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/_U7NJax4HHlo/SeuUnytVTbI/AAAAAAAACS8/NaHUMqDvDu0/s72-c/Colours.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-6799803545882028191</id><published>2009-04-08T12:00:00.000-07:00</published><updated>2009-04-11T13:44:45.840-07:00</updated><title type='text'>Adventures in 3D: Part VII - Matrix Revolutions</title><content type='html'>&lt;span style="font-style:italic;"&gt;(What dastardly cunning, a piece about matrices on the &lt;a href="http://xkcd.com/566/"&gt;10th anniversary of The Matrix&lt;/a&gt;)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;I already warned you that there was matrix maths coming up, so hold on to your hats. Once you've &lt;a href="http://chortle.ccsu.edu/VectorLessons/vmch13/vmch13_1.html"&gt;got to know them&lt;/a&gt;, you'll see that the principle of matrices is actually pretty simple, in that they just encode relatively complex equations in a simple form. Teaching matrix maths is outside the scope of this series, so I'll trust you'll do your own reading and just dive on in. I've also &lt;a href="http://www.cs.princeton.edu/introcs/95linear/Matrix.java.html"&gt;borrowed a Matrix class&lt;/a&gt;, as building our own is sure to be more of an education in bugfixing than 3D graphics.&lt;br /&gt;&lt;br /&gt;Our ultimate aim at the moment is to change our viewpoint - the code at the moment has us fixed in one position and able to spin the world.  We want to fix the world and be able to move around it, you know, like &lt;a href="http://www.newgrounds.com/portal/view/470460"&gt;this&lt;/a&gt;.  But first we'll slide in gently with using matrices, as they're a handy way to handle rotation.&lt;br /&gt;&lt;br /&gt;Naturally you can, at the drop of a hat, quote the formula for rotation of points around an axis in 3D. Around the X axis, that is&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;y[i] = (y[i] * Math.cos(r)) - (z[i] * Math.sin(r));&lt;br /&gt;z[i] = (z[i] * Math.cos(r)) + (y[i] * Math.sin(r));&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Assuming we represent our 3D points as a column matrix [x,y,z]T, these two equations can be neatly made into a matrix:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;| 1    0      0   |&lt;br /&gt;| 0 cos(r) -sin(r)| &lt;br /&gt;| 0 sin(r)  cos(r)|&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;and we can also do the same for rotation around Y and Z axes:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;|cos(r)  0  sin(r)|&lt;br /&gt;|   0    1    0   | &lt;br /&gt;|-sin(r) 0  cos(r)|&lt;br /&gt;&lt;br /&gt;|cos(r) -sin(r) 0 |&lt;br /&gt;|sin(r) cos(r)  0 | &lt;br /&gt;|   0    0      1 |&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;So with our new Matrix class we can, for any given angle r, construct a matrix that encodes the rotation around the appropriate axis.  When rotate() is called on the BasicSceneObject, we can build that matrix, and I'll add an overloaded form of rotate() on the abstract SceneObject so we can pass in that matrix to do the rotation.  We push our points into a 1x3 matrix, multiply that by the 3x3 rotation matrix, then get the values from the result matrix and put those back into our points.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public void rotate(RotationMatrix rotationMatrix) {&lt;br /&gt; Matrix[] points = new Matrix[3];&lt;br /&gt; &lt;br /&gt; for(int i=0;i&lt;3;i++) {&lt;br /&gt;  points[i] = new Matrix(new double[][] {{x[i]}, {y[i]}, {z[i]}});&lt;br /&gt;  Matrix result = rotationMatrix.times(points[i]);&lt;br /&gt;  x[i] = result.get(0,0);&lt;br /&gt;  y[i] = result.get(1,0);&lt;br /&gt;  z[i] = result.get(2,0);&lt;br /&gt; }&lt;br /&gt; &lt;br /&gt; normal = getNormal().normalise();&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;To build the rotation matrix, I created a RotationMatrix class, which is really just a utility class for building the matrices specified above, given an angle and an axis of rotation.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public static RotationMatrix getInstance(double theta, RotationAxis axis) {&lt;br /&gt; switch(axis) {&lt;br /&gt;  case X:&lt;br /&gt;   return new RotationMatrix(new double[][]  {&lt;br /&gt;                  {1,   0,          0},&lt;br /&gt;    {0,cos(theta),-sin(theta)},&lt;br /&gt;    {0,sin(theta),cos(theta)} });&lt;br /&gt;  case Y:&lt;br /&gt;   return new RotationMatrix(new double[][] { &lt;br /&gt;    {cos(theta),0,sin(theta)},&lt;br /&gt;    {0,         1,    0},&lt;br /&gt;    {-sin(theta),0,cos(theta)} });&lt;br /&gt;  case Z:&lt;br /&gt;   return new RotationMatrix(new double[][] {&lt;br /&gt;           {cos(theta),-sin(theta),0},&lt;br /&gt;    {sin(theta),cos(theta),0},&lt;br /&gt;    {0,         0,         1} });&lt;br /&gt; }&lt;br /&gt; return null;&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;The final piece is to create the matrix and pass it to rotate() when the mouse is moved.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;RotationMatrix yRot = RotationMatrix.getInstance(xangle, RotationAxis.Y);&lt;br /&gt;RotationMatrix xRot = RotationMatrix.getInstance(yangle, RotationAxis.X);&lt;br /&gt;for (SceneObject d : scene) {&lt;br /&gt; d.rotate(yRot);&lt;br /&gt; d.rotate(xRot);&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Give that a bash, and watch in amazement as your scene does exactly the same thing that it's always done.  Except in a bit of a neater way.  Which is no bad thing, right?  But we can make it even better than that.  One lovely property of matrices is that if you have two matrices to do two rotations, you can just multiply the two matrices and get a single matrix that does both rotations in one step:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;RotationMatrix yRot = RotationMatrix.getInstance(xangle, RotationAxis.Y);&lt;br /&gt;RotationMatrix xRot = RotationMatrix.getInstance(yangle, RotationAxis.X);&lt;br /&gt;RotationMatrix totalRot = xRot.times(yRot);&lt;br /&gt;for (SceneObject d : scene) {&lt;br /&gt; d.rotate(totalRot);&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Great stuff.  I love you matrices.  If you love matrices too, &lt;a href="http://revbingo.f2s.com/blogfiles/PartVII.zip"&gt;download the source&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-6799803545882028191?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/6799803545882028191/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=6799803545882028191' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/6799803545882028191'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/6799803545882028191'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2009/04/adventures-in-3d-part-vii-matrix.html' title='Adventures in 3D: Part VII - Matrix Revolutions'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-8023694385020679048</id><published>2009-04-04T13:02:00.000-07:00</published><updated>2009-04-07T14:10:43.800-07:00</updated><title type='text'>Adventures in 3D: Interlude</title><content type='html'>As mentioned in the &lt;a href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-intro.html"&gt;intro&lt;/a&gt; to this series, this is largely an unplanned foray into 3D graphics, and the code presented here is as I write it, without necessarily being the best or neatest way to do things.  At this point, I feel a need to do some refactoring on the code to try and make a solid base to build on further.  These bits are not necessarily necessary or instructive, so feel free to skip this bit if you like, although if you're going to carry on I suggest you download the source so that future entries make sense.  Even if you do continue reading, there are some bits that I'm simply going to point out rather than describe why I've made those decisions. &lt;br /&gt;&lt;br /&gt;So far we've only created one object, our lonely spheroid.  Naturally you are already deep into planning your own FPS that will, like, &lt;b&gt;totally&lt;/b&gt; make Halo look like Wolfenstein in comparison, and so we'll need to start thinking about creating and managing multiple objects.  An object is simply a bunch of polygons that are glued together - when the object moves 3 units to the left, all the polygons in that object move 3 units to the left.  Let's change the class hierarchies around a bit. Top of the tree is a SceneObject, something that can be a) rotated and b) drawn.  That extends to two classes. A BasicSceneObject is an object such as a sphere or a cube, or some other 3D shape we may wish to create.  BasicSceneObjects are really just a way of group together the other type of SceneObject, which is a Primitive, the 2D polygons that make up that object - to all intents and purposes, this is our Triangle class, although we could choose to render objects with squares, pentagons, or &lt;a href="http://en.wikipedia.org/wiki/Icosagon"&gt;icosagons&lt;/a&gt; if we so choose.&lt;br /&gt;&lt;br /&gt;The BasicSceneObject class defines how to draw and rotate multiple Primitives - just loop over every Primitive in the object, and also allows a method to get at it's polygons. This becomes important because when sorting for z-order, we need to consider all polygons in the scene together at the same time, for instance if two objects overlap.  The BSO also defines offsets for x,y,z, which defines where the object is in the world, and a translate() method to move the object.&lt;br /&gt;&lt;br /&gt;Now we've got a basis for objects, we can add some concrete object classes. We simply extend BasicSceneObject, and in the constructor define how to build it in terms of Primitives.  So there's a Spheroid object, which just uses the code we had in createScene() previously, and a Cuboid.  The Cuboid just defines 12 polygons (6 faces of 2 triangles).  The createScene() method now just becomes pretty simple:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;    BasicSceneObject sphere = new Spheroid(100,60,60,50);&lt;br /&gt;    sphere.translate(-100, -30, 100);&lt;br /&gt;    BasicSceneObject sphere2 = new Spheroid(40,30,10,50);&lt;br /&gt;    sphere2.translate(50, 20, -10);    &lt;br /&gt;    BasicSceneObject cube = new Cuboid(40,40,40);&lt;br /&gt;    cube.translate(100, 100, 30);&lt;br /&gt;    &lt;br /&gt;    scene.add(sphere);&lt;br /&gt;    scene.add(sphere2);&lt;br /&gt;    scene.add(cube);&lt;/pre&gt;&lt;br /&gt;&lt;center&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_U7NJax4HHlo/SdfOovMhoZI/AAAAAAAACS0/61Dtgrop6v0/s1600-h/3objects.png"&gt;&lt;img style="cursor:pointer; cursor:hand;width: 300px; height: 225px;" src="http://1.bp.blogspot.com/_U7NJax4HHlo/SdfOovMhoZI/AAAAAAAACS0/61Dtgrop6v0/s320/3objects.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5320948684020621714" /&gt;&lt;/a&gt;&lt;/center&gt;&lt;br /&gt;&lt;br /&gt;On the lighting front, until now the light has simply been a hardcoded vector in the Triangle class. Now there's a LightScene object, which could contain multiple Lights.  A Light has a direction, a position (which is not yet taken into account), and a colour (also not used yet), and as things progress may have some other characteristics specific to a particular type of lighting (e.g. an ambient light, spotlight etc.).  The LightScene is passed to a light() method on the Primitive class to decide what color the polygon should be rendered with.&lt;br /&gt; &lt;br /&gt;The final notable change is that you may have noticed the Y-axis problem. That is, traditionally the Y axis points up.  But in Java 2D, the Y axis goes down the screen, so essentially we're rendering the scene upside down. There's a simple answer to this, and it's back in the &lt;a href="http://java.sun.com/j2se/1.4.2/docs/api/java/awt/geom/AffineTransform.html"&gt;AffineTransform&lt;/a&gt; class we first met way back in &lt;a href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-i-basics.html"&gt;Part I&lt;/a&gt;. That time, we cheated by moving the axis origin to the centre of the screen with the Graphics2D.translate() method, so we never had to actually touch the AffineTransform ourselves. This time, we'll create an actual AffineTransform which represents a matrix:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;    |  1    0   width/2  |&lt;br /&gt;    |  0   -1  height/2  |&lt;br /&gt;    |  0    0     1      |&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;which, once you've &lt;a href="http://chortle.ccsu.edu/VectorLessons/vmch13/vmch13_1.html"&gt;famliarised yourself with matrix maths&lt;/a&gt;, you'll see means&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;x' = x + (width/2)&lt;br /&gt;y' = -y + (height/2)&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Although we're not explicitly stating it as such, this is a model-to-device transformation, the last step in our pipeline. As a minor optimisation, the AffineTransform object is created ahead of time and reused in each call to paintComponent(), rather than a new Transform object being created each time.  However, as the panel can be resized, we catch the call to setBounds() and recreate the Transform when required.&lt;br /&gt;&lt;br /&gt;Now that things are a bit better defined, &lt;a href="http://www.revbingo.f2s.com/blogfiles/Interlude.zip"&gt;download the source&lt;/a&gt; and let's move on.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-8023694385020679048?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/8023694385020679048/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=8023694385020679048' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/8023694385020679048'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/8023694385020679048'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2009/04/adventures-in-3d-interlude.html' title='Adventures in 3D: Interlude'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://1.bp.blogspot.com/_U7NJax4HHlo/SdfOovMhoZI/AAAAAAAACS0/61Dtgrop6v0/s72-c/3objects.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-4064637752135576242</id><published>2009-03-31T21:07:00.000-07:00</published><updated>2009-04-01T12:19:25.538-07:00</updated><title type='text'>Adventures in 3D: Part VI - Faster Pussycat! Cull! Cull!</title><content type='html'>&lt;span style="font-style: italic;"&gt;Oh look, the return of the Adventures in 3D series - you might want to &lt;a href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-intro.html"&gt;start at the beginning&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Last time out, we had a nice shaded sphere spinning around.  First things first, a sphere is very boring to rotate - if it weren't for the polygons, you wouldn't even notice.  So lets jam a little bit of maths in there so that it's a bit more obvious.  In the createScene() method, add a factor to the calculation of each Point position to modify the rotundness. Note the 0.8 and 0.4.  This will give you something akin to a &lt;a href="http://www.sweetiebag.com/product_images/full/softmint%20peppermint.jpg"&gt;Trebor Softmint&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;points[phi][theta] = new Point(radius * Math.cos(stp * phi)&lt;br /&gt;   * Math.sin(stp * theta), radius * 0.8 * Math.sin(stp * phi)&lt;br /&gt;   * Math.sin(stp * theta), radius * 0.4 * Math.cos(stp * theta));&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;If you have any sense of adventure, you will probably already have played around with the line&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;int STP = 60;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;in the createScene() method.  This line determines how many latitude/longtitude sections the sphere is divided into, and therefore, the higher this number, the more polygons that make up the sphere, and the smoother it will appear. With a value of 10, the sphere has a total of 100 polygons and will appear very obviously made up of triangles, much like the 3D graphics of yore.  A value of 100 will give you something much better looking, using 10000 polygons.  Of course, this is all a trade-off - the time to render 10000 polygons is not insignificant, and you'll find that the rotation is much less responsive.&lt;br /&gt;&lt;br /&gt;Polygon count is the rawest measure of 3D graphics performance. More polygons, in theory, equals better graphics. But you can't just up the numbers, because it all takes time. So you can try to find a way to render polygons on the screen quicker, or you can reduce the number of polygons you have to render. If you want to take the latter, you can either make your graphics less detailed, or you can just plain cheat.  Guess which option we're going for?&lt;br /&gt;&lt;br /&gt;In any given frame in our 3D scene, there are a bunch of polygons that we're just not going to see, which are those on the other side of the shape, facing away from us. In the case of a sphere, that's fully half of all the polygons we're drawing every frame which we don't need to draw.  So there's a simple conclusion - let's not draw them.  This is &lt;a href="http://en.wikipedia.org/wiki/Back-face_culling"&gt;Backface Culling&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Working out which faces you don't have to draw turns out to be very simple.  You'll remember that in &lt;a href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-iv-let-there-be.html"&gt;Part IV&lt;/a&gt; we discussed normals, which are vectors telling us which way a polygon is facing, and the dot product, which tells us something about whether two vectors are pointing the same way or not. To check for a backwards facing polygon, we'll compare it's vector normal to a vector that represents the direction we're looking in.  In the Triangle class:&lt;br /&gt;&lt;pre&gt;&lt;br /&gt; private Vector viewer = new Vector(0,0,-1);&lt;br /&gt; &lt;br /&gt; public void draw(Graphics2D graphics) {&lt;br /&gt;    Vector normal = getNormal().normalise();&lt;br /&gt;  &lt;br /&gt;    //backface culling&lt;br /&gt;    double cull = normal.dotProduct(viewer);&lt;br /&gt;    if(cull &gt; 0) return;&lt;br /&gt;&lt;br /&gt;    ...rest of the method...&lt;br /&gt; }&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Quite simply, get the vector normal for this polygon, normalise it (not strictly necessary here, but we'll reuse it later), and then find the dot product with the view vector.  Remember that a result of zero means that one vector is orthogonal (i.e. at 90 degrees) to the other. A result of greater than zero means that they form an angle less than 90 degrees, and so if the view vector is pointing away from the viewer, so is the vector normal, to some extent.  If that's the case here, we return from the method, so this polygon is not drawn at all on screen.&lt;br /&gt;&lt;br /&gt;There's one gotcha here - for the normal to point in the expected direction, the points in your polygons must be defined in a clockwise direction. If you define the points anti-clockwise, things can still work, but the vector normal for polygons facing away from you will point towards you, so the check becomes &lt;code&gt;if(cull &lt; 0)&lt;/code&gt; instead.&lt;br /&gt;&lt;br /&gt;Let's do some unscientific testing. I added some code (the static getAndResetDrawnCount() method) to the Triangle class so we can keep count of how many polygons have actually been drawn, and retrieve that to display at the end of each frame render.  I also added some very simple timing code to time how long each frame takes to display.&lt;br /&gt;&lt;br /&gt;Without backface culling present (and using an STP value of 100), the output shows that all 10000 polygons were rendered, and the time varies but a quick eye grep suggests it averages (on my machine) around 27-30ms.  When the two lines of culling code are added, the average number of objects actually drawn to the screen drops to around 5080, and the average time is somewhere around 15ms - as you might expect if it's doing half the work.&lt;br /&gt;&lt;br /&gt;By now you must surely be taking your seat in the pantheon of the 3D gods. If not, cheat by &lt;a href="http://revbingo.f2s.com/blogfiles/PartVI.zip"&gt;downloading the source&lt;/a&gt;. In either case, we'll be coming back to earth with a bump next time when we look at introducing a camera.  Warning: contains matrix maths.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-4064637752135576242?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/4064637752135576242/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=4064637752135576242' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/4064637752135576242'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/4064637752135576242'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/09/adventures-in-3d-part-vi-faster.html' title='Adventures in 3D: Part VI - Faster Pussycat! Cull! Cull!'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-4895747231103607707</id><published>2009-02-11T13:44:00.000-08:00</published><updated>2009-11-05T13:25:08.991-08:00</updated><title type='text'>Hot Gigolo (#324)</title><content type='html'>For many years I've led an extraordinary double life.  You wouldn't think it to look at me, but hiding under my mild mannered exterior is the equally mild mannered Reverend Bingo.  The good Reverend goes back &lt;a href="http://groups.google.co.uk/group/rec.music.rem/browse_thread/thread/e21a8f2689b54669?hl=en&amp;amp;ie=UTF-8&amp;amp;oe=utf-8&amp;amp;q=Mark+Rev+Bingo+rec.music.rem&amp;amp;pli=1"&gt;at least 13 years now&lt;/a&gt;, to my first steps on the net during my university years.   Heck, I even used it on some Telnet MUD type thing, the name of which is lost to time and memory. Over the years many have found deep meaning to the name - some kind of comment on the church and their relationship with modern culture - but the name &lt;a href="http://everything2.com/title/Bingo+Hand+Job"&gt;wasn't my invention&lt;/a&gt;. I seem to have borrowed it and never quite got round to giving it back.&lt;br /&gt;&lt;br /&gt;Generally I shorten it to revbingo, as in the url for this blog, and as far as I'm concerned it's everything a netname should be.  It's short, it's fairly memorable, it's interestingly cryptic and it's slightly silly without being downright embarrassing ("Ok Grandma, write this down - it's H-O-T-G-I-G-O-L-O-3-2-4 at gmail.com").  Most importantly, it's unique - I haven't found a website yet where I couldn't register with it as a username.&lt;br /&gt;&lt;br /&gt;At least, I thought it was unique.  One thing I've never really done in those 13 years is to google for "revbingo".   Let's give it a go, eh?  First two results, &lt;a href="http://www.amazon.co.uk/review/R1D39FQ5ICRY4N"&gt;Amazon review&lt;/a&gt;.  That's me alright - that book was &lt;span style="font-style: italic;"&gt;terrible&lt;/span&gt;&lt;span style="font-weight: bold;"&gt;&lt;span style="font-weight: bold;"&gt;.&lt;/span&gt;&lt;/span&gt;  Flickr, me.  RateMyCover, me. Ubuntu Forums, me. &lt;a href="http://www.anythingbutipod.com/forum/showthread.php?t=21165"&gt;Accidentally formatted my Sansa&lt;/a&gt;, m... wait!  I've never owned a Sansa, let alone wiped one accidentally.  Twitter, me. Spring RCP (ugh), me.  Ubuntu again, me.  Launchpad, me. Zoomr, me.  MoneySupermarket - not me.  Xandros - not me.  Rudius - not me.  Someone somewhere is polluting my brand.&lt;br /&gt;&lt;br /&gt;Now, having borrowed the name in the first place, I guess I ought to not be surprised if a few other people use it too.  But it brings home the importance we place on identity, especially in a context like the internet where &lt;span style="font-weight: bold;"&gt;real&lt;/span&gt; identity is all too difficult to establish.  How does someone googling for me on the net know what's me and what's not? There's nothing about the other posts that particularly concern me.  I guess I could live with folks thinking that I had &lt;a href="http://www.moneysupermarket.com/community/forums/t/car-accident-affect-on-my-insurance-premiums-24482.aspx"&gt;reversed into a van&lt;/a&gt; or tried to install &lt;a href="http://forums.xandros.com/viewtopic.php?t=34175&amp;amp;sid=d34e6f498e743e9954bd71c074017d0e"&gt;Half-Life&lt;/a&gt; on Linux (although let's be clear, I would never admit to having been a Linux n00b :).  It's just that it's not me. The fact that these other uses only pop up here and there in amongst my own usage make it even more likely that someone would believe it's me.&lt;br /&gt;&lt;br /&gt;Which makes me wonder, where have these people gone since?  Have they discovered that I've already taken that username on gmail, twitter, flickr et al and gone for something else? (In a slight panic, I've just bought up www.revbingo.com).  Did they decide that revbingo was a really poor username?&lt;br /&gt;&lt;br /&gt;I guess there's nothing else for it.  Just call me HotGigolo324.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-4895747231103607707?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/4895747231103607707/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=4895747231103607707' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/4895747231103607707'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/4895747231103607707'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2009/02/hot-gigolo-324.html' title='Hot Gigolo (#324)'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-3656755156244229577</id><published>2008-10-27T13:10:00.000-07:00</published><updated>2008-10-27T13:27:31.540-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='knowledgebin'/><category scheme='http://www.blogger.com/atom/ns#' term='xbox'/><title type='text'>Case in point</title><content type='html'>This definitely comes under the Bin Of Knowledge heading. In a google I found many people with this problem, but no mention of my particular solution, so maybe this will help at least a handful of folks out there.&lt;br /&gt;&lt;br /&gt;I have an Xbox360 through which I stream content from a &lt;a href="http://www.lacie.com/products/product.htm?pid=10844"&gt;LaCie EDMini&lt;/a&gt; NAS disk, which serves up files via &lt;a href="http://www.twonkyvision.de/Products/TwonkyMedia/"&gt;TwonkyMedia&lt;/a&gt;. To be honest, it's usually music, but last night I felt the urge to do some learning and so decided to watch some episodes of &lt;a href="http://meetthegimp.org/"&gt;Meet The GIMP&lt;/a&gt; (highly recommended by the way, if you enjoy listening to earnest Germans discuss the finer details of sRGB colour space - and who doesn't?).  These come as .mp4 files, but the Xbox mysteriously didn't seem to recognise them, even though they're supposed to be supported. That was until I downloaded one episode that had an .MP4 extension (note the capitalisation), which did appear. &lt;br /&gt;&lt;br /&gt;This led to one of those "tch, I know this isn't going to work, but I'll try it on the crazy off-chance" moments, where I renamed the files to end with the .MP4 extension instead of .mp4.  And voila, there they were on the Xbox.&lt;br /&gt;&lt;br /&gt;I'm not sure where the problem lies on this one, whether the Xbox or Twonky, but don't say I don't give you things.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-3656755156244229577?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/3656755156244229577/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=3656755156244229577' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/3656755156244229577'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/3656755156244229577'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/10/case-in-point.html' title='Case in point'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-3979288430136826828</id><published>2008-08-31T12:40:00.001-07:00</published><updated>2009-04-01T05:16:37.266-07:00</updated><title type='text'>Adventures in 3D: Part V - Law and Z-Order</title><content type='html'>At the end of Part IV, we finally had a rotating sphere that had some nice lighting from one side.  But there was a problem.  If you turned the sphere, you'd almost certainly see sections where it looked like you were looking through the sphere at the surfaces on the back side.&lt;br /&gt;&lt;br /&gt;There's a simple reason for this, and a simple fix.  The problem is that we're just drawing polygons relatively willy nilly to the screen - the order in which they appear in the ArrayList that we use for the scene. So in some cases we draw the front side polygons, but then also draw the back side polygons, which just appears over the top of the front side.  What we need to do is make sure that anything at the back is drawn first, then anything in front of it is drawn last. This is &lt;a href="http://en.wikipedia.org/wiki/Z-order"&gt;z-ordering&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;The fact that we've got all our scene objects in an ArrayList is very handy.  We can just use Collections.sort() to sort our list into Z-order before we draw the polygons.  To ease things along, I'm going to rearrange the class hierarchy sightly (read: the original design wasn't really thought through).  I'll implement an abstract superclass SceneObject - anything that is in our scene, whether it be Points, Triangles, Spaceships, whatever, needs to be a) Drawable, and b) Comparable, so we can work out the order to draw it. So our scene now becomes an ArrayList&amp;lt;SceneObject&amp;gt; instead of ArrayList&amp;lt;Drawable&amp;gt;. To make the compareTo() method work, it'll also have an abstract method getZOrder(), which will be used in the comparison.  Because it's declared in the superclass, it means I can still compare Points with Triangles and Spaceships with Pineapples, at least as far as Z order is concerned.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;import java.awt.Graphics2D;&lt;br /&gt;&lt;br /&gt;public abstract class SceneObject implements Drawable, Comparable&amp;lt;SceneObject&amp;gt; {&lt;br /&gt;&lt;br /&gt; @Override&lt;br /&gt; public abstract void draw(Graphics2D g);&lt;br /&gt; public abstract double getZOrder();&lt;br /&gt; &lt;br /&gt; @Override&lt;br /&gt; public int compareTo(SceneObject o) {&lt;br /&gt;  double zOrder1 = this.getZOrder();&lt;br /&gt;  double zOrder2 = o.getZOrder();&lt;br /&gt;  if(zOrder1 == zOrder2){&lt;br /&gt;   return 0;&lt;br /&gt;  }else{&lt;br /&gt;   return (zOrder1 &amp;gt; zOrder2) ? -1 : 1;&lt;br /&gt;  }&lt;br /&gt; }&lt;br /&gt; &lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;To keep things simple, we just work out the &lt;span style="font-weight: bold;"&gt;average&lt;/span&gt; z distance for the Triangle and use that value.  In more complicated 3D scenes, it is of course possible for triangles to overlap, but we'll assume here that everything tessalates nicely. So implementing the getZOrder() method in our Triangle class is very easy.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt; public double getZOrder() {&lt;br /&gt;  return (z[0] + z[1] + z[2])/3;&lt;br /&gt; }&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Now that all that's done, the panel class just calls Collections.sort(scene) before actually drawing the scene.  Note that in terms of performance it's probably hard to do much better than Collections.sort(). As one last change, let's light the sphere from the front, so that you can see the whole thing in all it's glory.  That just means changing the light vector from (1,0,0) to (0,0,1).  You can also check that your z-ordering is working by moving it to (0,0,-1).  If the light is behind the sphere, and your polygons are being painted correctly, you should see nothing.&lt;br /&gt;&lt;br /&gt;&lt;/drawable&gt;&lt;/sceneobject&gt;&lt;div style="text-align: center;"&gt;&lt;sceneobject&gt;&lt;drawable&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/_U7NJax4HHlo/SLsDnc4tN6I/AAAAAAAABkQ/uKhNPs74V4E/s1600-h/PartVa.png"&gt;&lt;img style="cursor: pointer;" src="http://4.bp.blogspot.com/_U7NJax4HHlo/SLsDnc4tN6I/AAAAAAAABkQ/uKhNPs74V4E/s320/PartVa.png" alt="" id="BLOGGER_PHOTO_ID_5240786567679522722" border="0" /&gt;&lt;/a&gt;&lt;/drawable&gt;&lt;/sceneobject&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;Yes, you are l33t. If you have failed at being l33t, you can at least pretend by &lt;a href="http://www.revbingo.f2s.com/blogfiles/PartV.zip"&gt;downloading the source&lt;/a&gt;. Then you can move on to &lt;a href="http://revbingo.blogspot.com/2008/09/adventures-in-3d-part-vi-faster.html"&gt;Part VI - Faster Pussycat! Cull! Cull!&lt;/a&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-3979288430136826828?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/3979288430136826828/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=3979288430136826828' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/3979288430136826828'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/3979288430136826828'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-v-law-and-z-order.html' title='Adventures in 3D: Part V - Law and Z-Order'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://4.bp.blogspot.com/_U7NJax4HHlo/SLsDnc4tN6I/AAAAAAAABkQ/uKhNPs74V4E/s72-c/PartVa.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-8904793972480214770</id><published>2008-08-31T01:29:00.000-07:00</published><updated>2009-04-01T05:30:15.681-07:00</updated><title type='text'>Adventures in 3D: Part IV - Let There Be Light</title><content type='html'>&amp;lt;&lt;a href="http://en.wikipedia.org/wiki/Swiss_Toni"&gt;SwissToni&lt;/a&gt;&amp;gt; You see, creating 3D objects is like making love to a beautiful woman&amp;lt;/SwissToni&amp;gt;.  You've got to set the mood, a bit of gentle lighting here and there, create some contrast.&lt;br /&gt;&lt;br /&gt;Simple lighting is actually easy stuff, just wave hello to our friend, the Vector. Remember from Part I that the Vector simply consists of measurements along the x,y,z axes, and you can crunch some numbers to get a couple of useful things out of it, which we'll come to.&lt;br /&gt;&lt;br /&gt;Consider a single light source in our scene.  It's easy enough to see that any faces that are pointing directly towards the light will be fully lit, and any that are pointing away from the light will be in the dark.  In between those, faces that are at some angle towards the light will be partially lit, in some proportion that is a function of that angle.&lt;br /&gt;&lt;br /&gt;In 3D space, you can get an idea of how much a given face is pointing in a chosen direction by calculating the dot product.  The dot product gives you just a number (a scalar value). If you vectors are of unit length, then the dot product will range from -1 (the vectors are in opposite directions) to 0 (the vectors are at right angles to each other) to +1 (the vectors are in the same direction). The dot product is stupidly easy to work out, it's just multiplication and addition.  Given vectors A(x,y,z) and B(x',y',z'), the dot product is defined as:&lt;br /&gt;&lt;br /&gt;A.B = x*x' + y*y' + z*z'&lt;br /&gt;&lt;br /&gt;Let's start putting this into our code.  We'll need to define a Vector class, which is just x,y,z values, plus a method to calculate that dot product.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public class Vector {&lt;br /&gt;&lt;br /&gt; public double x;&lt;br /&gt; public double y;&lt;br /&gt; public double z;&lt;br /&gt; &lt;br /&gt; public Vector(double x, double y, double z){&lt;br /&gt;  this.x = x;&lt;br /&gt;  this.y = y;&lt;br /&gt;  this.z = z;&lt;br /&gt; }&lt;br /&gt; &lt;br /&gt; public double dotProduct(Vector other){&lt;br /&gt;  return this.x * other.x + this.y * other.y + this.z * other.z;&lt;br /&gt; }&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;So far, so good.  We know we need to calculate the dot product, but what Vectors are we using?  One is obviously going to be a Vector representing the direction of the light, and that's simple because we just need to define an arbitrary Vector.  What we need to know is how that compares to the direction that each surface in our scene is pointing.  For that, we need to calculate the cross product.  The cross product of two vectors results in another Vector that points at right angles to those original Vectors.  Again, it's startlingly simple to work out.  Take A(x,y,z) and B(x',y',z') again, and you can define a new Vector C(x'',y'',z'') where:&lt;br /&gt;&lt;br /&gt;x'' = y*z' - z*y'&lt;br /&gt;y'' = z*x' - x*z'&lt;br /&gt;z'' = x*y' - y*x'&lt;br /&gt;&lt;br /&gt;So let's add that into our Vector class as well:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public Vector crossProduct(Vector other){&lt;br /&gt;&lt;br /&gt;  Vector v = new Vector();&lt;br /&gt;&lt;br /&gt;  v.x = (this.y * other.z) - (this.z * other.y);&lt;br /&gt;  v.y = (this.z * other.x) - (this.x * other.z);&lt;br /&gt;  v.z = (this.x * other.y) - (this.y * other.x);&lt;br /&gt;  &lt;br /&gt;  return v;&lt;br /&gt; }&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Given two vectors in the plane of our triangle, the cross product will give you a vector that points perpendicular to the triangle (a.k.a the &lt;a href="http://en.wikipedia.org/wiki/Surface_normal"&gt;Surface Normal&lt;/a&gt;).  And how do you get two Vectors in the plane of the triangle?  Simple, just work out vectors between the points of the triangle. A Vector is just the difference between two points.  We can add a useful method to our Point class to return a Vector given another Point.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public Vector vectorTo(Point p){&lt;br /&gt;  return new Vector(this.x - p.x, this.y - p.y, this.z - p.z);&lt;br /&gt; }&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;and given that method, we can take the three points in our triangle, calculate two vectors, get their cross product and wa la, you have your surface normal. A bit like this...&lt;br /&gt;&lt;br /&gt;&lt;pre&gt; public Vector getNormal(){&lt;br /&gt;  &lt;br /&gt;  Point p1 = new Point(x[0], y[0], z[0]);&lt;br /&gt;  Point p2 = new Point(x[1], y[1], z[1]);&lt;br /&gt;  Point p3 = new Point(x[2], y[2], z[2]);&lt;br /&gt;  &lt;br /&gt;  Vector edge1 = p2.vectorTo(p1);&lt;br /&gt;  Vector edge2 = p3.vectorTo(p2);&lt;br /&gt;&lt;br /&gt;  &lt;br /&gt;  return edge1.crossProduct(edge2);&lt;br /&gt; }&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Note that the creation of the Points and use of the vectorTo() method could actually be done away with, and we could just create a vector using e.g. new Vector(x[1]-x[0],y[1]-y[0],z[1]-z[0]), but it helps demonstrate the concepts.&lt;br /&gt;&lt;br /&gt;Right, we have our surface normal, and we have our arbitrary light vector.  Let's for now say that the light vector L is (1,0,0) i.e. the light is from the right.  We're going to get the dot product to see just how parallel they are, and then use that to create a colour to use for the triangle.  For now we'll just stick with greyscale, and it makes sense to use Hue,Saturation,Brightness (HSB) instead of RGB - then we can just alter the Brightness based on the dot product.  When setting the colour, Brightness needs to be between 0.0 and 1.0, so we'll need to deal with vectors of unit length, otherwise the dot product will be outside that range.  That calls for normalisation.  To normalise a vector simply means to keep it pointing in the same direction, but make it's length equal to 1. Don't confuse normals (perpendicular vectors) with normalised vectors (vectors scaled to unit length). Let's add a couple of methods to the Vector class.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;  public Vector normalise () {&lt;br /&gt;     return scale (1.0 / Math.sqrt (dotProduct(this)));&lt;br /&gt;  }&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;  public Vector scale (double scale) {&lt;br /&gt;     return new Vector (x * scale, y * scale, z * scale);&lt;br /&gt;  }&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;The scale() method just scales the vector along each axis.  Note the trick to get the length of the vector - the dot product of a vector with itself is the length squared.  With these methods, we can now make sure that we're dealing with two unit vectors in the Triangle's draw() method.  To summarise the whole thing again - we'll take the triangle's surface normal and normalise it, then take the light vector and normalise it, then find the dot product between the two.  Given that they're both normalised, we'll get a number between -1 and 1, which will be used to set the brightness of the colour that's used to draw the polygon.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;     double dt = getNormal().normalise().dotProduct(normLight);&lt;br /&gt;     Color c = Color.getHSBColor(0.0f, 0.0f, (float) dt);&lt;br /&gt;     graphics.setColor(c);&lt;br /&gt;     graphics.fillPolygon(xPoints, yPoints, 3);&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;&lt;div style="text-align: center;"&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_U7NJax4HHlo/SLrp8GkGIBI/AAAAAAAABj4/Upo1D_5f0Qw/s1600-h/PartIVa.png"&gt;&lt;img style="cursor: pointer;" src="http://3.bp.blogspot.com/_U7NJax4HHlo/SLrp8GkGIBI/AAAAAAAABj4/Upo1D_5f0Qw/s320/PartIVa.png" alt="" id="BLOGGER_PHOTO_ID_5240758335162425362" border="0" /&gt;&lt;/a&gt;&lt;/div&gt; FAIL!  Well, two thirds fail at least.  It's looking fairly cool on the left side, not so hot on the right side.  That's because everything on the right hand side is returning a number less than zero, which obviously causes the getHSBColor() method to go a bit crazy (actually, I'm surprised it didn't just bork altogether).  So we plainly need to have some sort of check in there to make sure we only deal with positive numbers.  Or wait, do we?  Remember that we set our light source at (1,0,0), so the light should be coming from the right - right?  The thing is, the surface normals for polygons will be pointing to the right.  The light vector is coming from the right, pointing left.  So actually, the correct polygons to be lit are those that have &lt;span style="font-weight: bold;"&gt;negative&lt;/span&gt; dot products i.e. the vectors are in opposite directions.  So let's add a quick check.  Anything with a zero or positive dot product is on the dark side of the sphere, so we'll just draw them black.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;     Color c = (dt &amp;lt; 0) ?&lt;br /&gt;      Color.getHSBColor(0.0f, 0.0f, (float) Math.abs(dt)) :&lt;br /&gt;      Color.BLACK;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;&lt;div style="text-align: center;"&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_U7NJax4HHlo/SLrsNua87KI/AAAAAAAABkI/BRiyFipyNiQ/s1600-h/PartIVb.png"&gt;&lt;img style="cursor: pointer;" src="http://3.bp.blogspot.com/_U7NJax4HHlo/SLrsNua87KI/AAAAAAAABkI/BRiyFipyNiQ/s320/PartIVb.png" alt="" id="BLOGGER_PHOTO_ID_5240760836942523554" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;I like! If you like too, &lt;a href="http://www.revbingo.f2s.com/blogfiles/PartIV.zip"&gt;download the source&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Wait a second though.  Have a go at rotating the object.  Sure, it's prettily lit, but at some point you'll find something a bit odd going on.  In &lt;a href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-v-law-and-z-order.html"&gt;Part V - Law And Z-Order&lt;/a&gt;, we'll talk Z-order.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-8904793972480214770?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/8904793972480214770/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=8904793972480214770' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/8904793972480214770'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/8904793972480214770'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-iv-let-there-be.html' title='Adventures in 3D: Part IV - Let There Be Light'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/_U7NJax4HHlo/SLrp8GkGIBI/AAAAAAAABj4/Upo1D_5f0Qw/s72-c/PartIVa.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-5636563878393169324</id><published>2008-08-30T12:43:00.001-07:00</published><updated>2009-04-01T05:14:32.284-07:00</updated><title type='text'>Adventures in 3D: Part III - Poly Filler</title><content type='html'>Right, you've done parts I and II, and you've got some rotating points.  Remember that in Part I, we discussed that we were going to build our scenes out of triangles.  Luckily for us, we also mentioned that a triangle is just 3 points.  If you can rotate a point, you can rotate a triangle.&lt;br /&gt;&lt;br /&gt;To start with, we'll throw together a Triangle class.  It'll have arrays to store the coordinates for each point, and when we rotate it we just need to apply the rotation to each element of the array.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;import java.awt.Graphics2D;&lt;br /&gt;&lt;br /&gt;public class Triangle implements Drawable {&lt;br /&gt;&lt;br /&gt;double[] x = new double[3];&lt;br /&gt;double[] y = new double[3];&lt;br /&gt;double[] z = new double[3];&lt;br /&gt;&lt;br /&gt;public Triangle(double x1, double y1, double z1, double x2, double y2, double z2,&lt;br /&gt;        double x3, double y3, double z3){&lt;br /&gt;x[0] = x1;&lt;br /&gt;x[1] = x2;&lt;br /&gt;x[2] = x3;&lt;br /&gt;y[0] = y1;&lt;br /&gt;y[1] = y2;&lt;br /&gt;y[2] = y3;&lt;br /&gt;z[0] = z1;&lt;br /&gt;z[1] = z2;&lt;br /&gt;z[2] = z3;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;public void draw(Graphics2D graphics){&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;public void rotateZ(double radians){&lt;br /&gt;  double[] sx = x.clone();&lt;br /&gt;  for(int i = 0; i &amp;lt; 3 ;i++){&lt;br /&gt;  x[i] = (sx[i] * Math.cos(radians)) - (y[i] * Math.sin(radians));&lt;br /&gt;  y[i] = (y[i] * Math.cos(radians)) + (sx[i] * Math.sin(radians));&lt;br /&gt;}&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:georgia;"&gt;Don't forget that when it comes to storing the original points in the &lt;/span&gt;&lt;span style="font-family:georgia;"&gt;rotation, assignment of an array is by reference, so you need to use .clone() to get a copy of the array instead of a reference to the original.  Again, a shrinking triangle will be a sign that you'd missed something.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:georgia;"&gt;We're also implementing Drawable, so we need to specify how to draw this triangle.  Luckily that's pretty simple, it's just a case of drawing a polygon between the appropriate points.  The only slight difficulty is that drawPolygon() expects arrays of integers, and we're storing the points as doubles, so we'll need to do a quick cast into new arrays before we can draw the object.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public void draw(Graphics2D graphics){&lt;br /&gt;&lt;br /&gt;   for(int i=0;i &amp;lt; 3;i++){&lt;br /&gt;      xPoints[i] = (int) x[i];&lt;br /&gt;      yPoints[i] = (int) y[i];&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;   graphics.setColor(Color.WHITE);&lt;br /&gt;   graphics.drawPolygon(xPoints, yPoints, 3);&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Last thing to do is to actually create some triangles to draw. We could do something really simple, but let's push the boat out and create a sphere.  I won't go into the details too much, but it just involves creating points by latitude and longitude, creating squares from neighbouring points, and then breaking that square down into 2 triangles, which are added to the scene. The STP variable determines how small those squares are, and so ultimately how smooth your object looks - at the cost of speed, natch. Play around with it and see what works.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;    private ArrayList&amp;lt;Drawable&amp;gt; createScene(){&lt;br /&gt;int STP = 60;&lt;br /&gt;double stp = Math.PI * 2 / STP;&lt;br /&gt;double radius = 100;&lt;br /&gt;ArrayList&amp;lt;Drawable&amp;gt; scene = new ArrayList&amp;lt;Drawable&amp;gt;();&lt;br /&gt;&lt;br /&gt;Point[][] points = new Point[STP][(STP/2)+1];&lt;br /&gt;for(int phi = 0; phi &amp;lt; STP; phi++){&lt;br /&gt;    for(int theta = 0; theta &amp;lt; (STP/2)+1; theta++){&lt;br /&gt;        points[phi][theta] = new Point(&lt;br /&gt;                  radius * Math.cos(stp*phi) * Math.sin(stp*theta),&lt;br /&gt;                  radius * Math.sin(stp*phi) * Math.sin(stp*theta),&lt;br /&gt;                  radius * Math.cos(stp*theta));&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;for(int x = 0; x &amp;lt; STP; x++){&lt;br /&gt;    for(int y = 0; y &amp;lt; (STP/2); y++){&lt;br /&gt;        int xx = (x+1)%STP;&lt;br /&gt;        int yy = (y+1);&lt;br /&gt;        Point p1 = points[x][y];&lt;br /&gt;        Point p2 = points[xx][y];&lt;br /&gt;        Point p3 = points[x][yy];&lt;br /&gt;        Point p4 = points[xx][yy];&lt;br /&gt;        Triangle t = new Triangle(p1.x, p1.y, p1.z, &lt;br /&gt;                        p2.x, p2.y, p2.z, &lt;br /&gt;                        p3.x, p3.y, p3.z);&lt;br /&gt;        Triangle t2 = new Triangle(p2.x, p2.y, p2.z, &lt;br /&gt;                        p4.x, p4.y, p4.z, &lt;br /&gt;                        p3.x, p3.y, p3.z);&lt;br /&gt;        scene.add(t);&lt;br /&gt;        scene.add(t2);&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;return scene;&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Some other slight adjustments to be made.  In our laziness we assumed in the mouseDragged() method that we were dealing with Points, but our scene is now made up of Triangles, so that needs to be changed.  You might also want to change the axes of rotation to whatever feels natural, now that you've only gone and created a freakin' wireframe sphere that rotates when you drag it!  &lt;a href="http://www.revbingo.f2s.com/blogfiles/PartIII.zip"&gt;Download the source&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;div style="text-align: center;"&gt; &lt;pre&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_U7NJax4HHlo/SLms9DJdm7I/AAAAAAAABjg/_iTvunZb_9A/s1600-h/PartIII.png"&gt;&lt;img style="cursor: pointer;" src="http://1.bp.blogspot.com/_U7NJax4HHlo/SLms9DJdm7I/AAAAAAAABjg/_iTvunZb_9A/s320/PartIII.png" alt="" id="BLOGGER_PHOTO_ID_5240409806239275954" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/pre&gt;&lt;div style="text-align: left;"&gt; One final thing - instead of wireframe, let's actually fill the polygons and create a solid object. Just replace graphics.drawPolygon() with graphics.fillPolygon().&lt;br /&gt;&lt;div style="text-align: center;"&gt;&lt;pre&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_U7NJax4HHlo/SLmvVKA3KVI/AAAAAAAABjo/lCtq5PBWv9E/s1600-h/PartIII_2.png"&gt;&lt;img style="cursor: pointer;" src="http://3.bp.blogspot.com/_U7NJax4HHlo/SLmvVKA3KVI/AAAAAAAABjo/lCtq5PBWv9E/s320/PartIII_2.png" alt="" id="BLOGGER_PHOTO_ID_5240412419422366034" border="0" /&gt;&lt;/a&gt;&lt;/pre&gt;&lt;/div&gt;Uhhhh.  Wait.  Now we've just got a big blob.  That's not nearly as cool.  Don't panic though, in &lt;a href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-iv-let-there-be.html"&gt;Part IV - Let There Be Light&lt;/a&gt;, we'll look at how to give this thing some definition.&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-5636563878393169324?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/5636563878393169324/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=5636563878393169324' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/5636563878393169324'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/5636563878393169324'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-iii-poly-filler.html' title='Adventures in 3D: Part III - Poly Filler'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://1.bp.blogspot.com/_U7NJax4HHlo/SLms9DJdm7I/AAAAAAAABjg/_iTvunZb_9A/s72-c/PartIII.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-5956302182370088166</id><published>2008-08-30T08:39:00.000-07:00</published><updated>2009-04-01T05:13:43.306-07:00</updated><title type='text'>Adventures in 3D: Part II - Round We Go</title><content type='html'>&lt;span style="font-style: italic;font-family:georgia;" &gt;[The next in a series on simple 3D graphics in Java.  You might want to read the &lt;/span&gt;&lt;a style="font-family: georgia; font-style: italic;" href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-intro.html"&gt;Intro&lt;/a&gt;&lt;span style="font-style: italic;font-family:georgia;" &gt;, and &lt;/span&gt;&lt;a style="font-family: georgia; font-style: italic;" href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-i-basics.html"&gt;Part I - The Basics&lt;/a&gt;&lt;span style="font-style: italic;font-family:georgia;" &gt;]&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;In Part I, we got some spots.  Not too impressive, and certainly not very 3D.  But let's build on that, and start making those spots move.  Our aim is to have something rotating when we move the mouse, so let's look at rotation.&lt;br /&gt;&lt;br /&gt;If you want to talk rotation, you need to talk &lt;a href="http://mathworld.wolfram.com/Sine.html"&gt;sine&lt;/a&gt; and &lt;a href="http://mathworld.wolfram.com/Cosine.html"&gt;cosine&lt;/a&gt;.  Remembering back to school days and trigonometry, sine and cosine together describe a circle.  If you want to know the x,y of any point around a circle of unit radius, you just need to look at the angle from the horizontal - the cosine of that angle tells you x, and the sine of the angle gives you y. If the circle is not unit radius, you just multiply accordingly. In simple terms:&lt;br /&gt;&lt;br /&gt;x = r cos t&lt;br /&gt;y = r sin t&lt;br /&gt;&lt;br /&gt;where t is the angle.  That's all very well, but if we're going to do arbitrary rotations, you need to talk in terms of the delta i.e. the change in angle, and not just an absolute angle.  Thankfully, that's not overly difficult either.  I know I said I wouldn't delve too much into the maths, but this is useful to know. You have a point x,y, which with the equations above you can talk about in terms of an (unknown) angle t.  Now, you want to rotate that point around an axis (the Z axis) by an arbitrary angle, which we'll call dt, and that will give you a new point x',y'.  That new point you can talk about in terms of an (unknown) angle t'.  Put all that together, and you come up with:&lt;br /&gt;&lt;br /&gt;x' = r cos t'&lt;br /&gt;y' = r sin t'&lt;br /&gt;&lt;br /&gt;But t' is just t + dt, so:&lt;br /&gt;&lt;br /&gt;x' = r cos (t + dt)&lt;br /&gt;y' = r sin (t + dt)&lt;br /&gt;&lt;br /&gt;Naturally, your maths teacher forced you to constantly chant the formulas:&lt;br /&gt;&lt;br /&gt;cos (t + dt) = cos t * cos dt - sin t * sin dt&lt;br /&gt;sin (t + dt) = sin t * cos dt + cos t * sin dt&lt;br /&gt;&lt;br /&gt;With a little bit of substitution and refactoring, you arrive at:&lt;br /&gt;&lt;br /&gt;x' = x * cos dt - y * sin dt&lt;br /&gt;y' = y * cos dt + x * sin dt&lt;br /&gt;&lt;br /&gt;and before you know it, you can talk about x' and y' purely in terms of the old position (x,y) and the angle you've rotated through (dt).&lt;br /&gt;&lt;br /&gt;Let's plug that into our Point class.  We'll create a method called rotateZ() which will accept an angle as a parameter.  The method will then move the point from x,y to x', y' by applying the formulas above.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public void rotateZ(double angle){&lt;br /&gt;    double x0 = x;&lt;br /&gt;    x = (x0 * Math.cos(angle) - y * Math.sin(angle));&lt;br /&gt;    y = (y * Math.cos(angle) + x0 * Math.sin(angle));&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Notice that we save the initial value of x in another variable beforehand.  Otherwise, we modify x in the first formula, and the formula for y will be messed up.  If your scene appears to shrink as you rotate it, you've probably made that mistake.&lt;br /&gt;&lt;br /&gt;Sweet.  Now we just have to do something to invoke this method and get our points moving.  We're going to do this fairly simply, by adding a MouseMotionListener to the panel.  When the mouse is dragged i.e. the button is down, we'll measure how far the mouse has travelled from it's last position, and then rotate all the points in our scene that number of degrees. Naturally we'll convert it into radians first (you &lt;span style="font-weight: bold;"&gt;are&lt;/span&gt; working in &lt;a href="http://en.wikipedia.org/wiki/Radian"&gt;radians&lt;/a&gt;, right?  Right??).&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public void mouseDragged(MouseEvent e) {&lt;br /&gt;   int x = e.getX();&lt;br /&gt;   int dx = x - oldX;&lt;br /&gt;   oldX = x;&lt;br /&gt;   double angle = dx * radian;&lt;br /&gt;   for(Drawable d : scene){&lt;br /&gt;       Point p = (Point) d;&lt;br /&gt;       p.rotateZ(angle);&lt;br /&gt;   }&lt;br /&gt;   panel.repaint();&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Of course, don't forget to a) store the last known X position for next time, and more importantly b) repaint the panel once the rotation is done so you can see the result of your hard work.&lt;br /&gt;&lt;br /&gt;If it all works, you should now have a bunch of spots that rotate when you drag the mouse left and right!  It gets better.  Let's say that when you move up and down, we should rotate around the X axis.  Well, that's easy peasy - we just shift the axes so Y becomes Z and X becomes Y, and then reuse the same equation (think about the &lt;a href="http://www.schorsch.com/kbase/glossary/right_hand_rule.html"&gt;Right Hand Rule&lt;/a&gt;).  So you can plug that formula in as well and link that to the mouse movement in the y direction.  If you're lazy, you could just &lt;a href="http://www.revbingo.f2s.com/blogfiles/PartII.zip"&gt;download the source&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;Wait, what's that?  It looks like it's rotating in 3D? Well sure it does!   Congrats space cadet, you're well on the way.  Best take a look at &lt;a href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-iii-poly-filler.html"&gt;Part III - Poly Filler&lt;/a&gt; before you explode with excitement.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-5956302182370088166?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/5956302182370088166/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=5956302182370088166' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/5956302182370088166'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/5956302182370088166'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-ii-round-we-go.html' title='Adventures in 3D: Part II - Round We Go'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-429800808627761618</id><published>2008-08-30T05:54:00.000-07:00</published><updated>2009-04-01T05:12:27.067-07:00</updated><title type='text'>Adventures in 3D: Part I - The Basics</title><content type='html'>&lt;span style="font-style: italic;"&gt;[If you haven't already, you might want to read the&lt;a href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-intro.html"&gt; Intro&lt;/a&gt;]&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;So, let's set off with some fairly modest aims - we just want to produce a simple 3D scene that we can rotate using the mouse.  There's a few simple concepts that we'll need to get under our belts to do that.  Thankfully, most of them are little more than GCSE maths.  If you want to really get a grasp on these, you could do a lot worse than &lt;a href="http://chortle.ccsu.edu/VectorLessons/vectorIndex.html"&gt;this page&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Let's also put a disclaimer on this.  This is my first real dive into this sort of stuff, and I make no claim to be an expert, or that what's included here is necessarily the fastest, easiest or correctest way to achieve the aims.  It's just a gentle amble trying to discover the key concepts and hopefully get something out the other end that looks reasonable.&lt;br /&gt;&lt;br /&gt;First concept: the Point.  A Point is exactly what you'd think it would be, a point in space.  It consists of x, y and z coordinates, and it can be translated in some way to another point in space.  It has location, but no length, and no direction.&lt;br /&gt;&lt;br /&gt;Second concept: the Vector.  A Vector can sometimes look suspiciously like a Point, but it's important not to confuse them. Like a Point, a Vector consists of x,y,z components, but the key difference is that these indicate a change in those axes, not a point.  Think of it as an arrow.  So a Vector (2,4,5) goes 2 units along the X axis, and 4 units along the Y axis, and 5 units along the Z axis.  It has a direction, and it has a length, but it doesn't exist in any particular location.  In that sense it's the precise opposite of a Point.  You can do funky things with a Vector.  You can take another Vector and calculate their &lt;a href="http://en.wikipedia.org/wiki/Dot_product"&gt;dot product&lt;/a&gt;, which gives you a measure of how &lt;a href="http://en.wikipedia.org/wiki/Orthogonal"&gt;orthogonal&lt;/a&gt; they are.  You can also take another Vector and find their &lt;a href="http://en.wikipedia.org/wiki/Cross_product"&gt;cross product&lt;/a&gt;, which will result in another Vector which points at right angles to the first two.  You can take a Vector and scale it, to double it's length, or halve it's length, or make it's length equal to one, a.k.a. normalisation.  Under the covers, all of these things involve nothing more than a bit of multiplication, but they are immensely powerful.&lt;br /&gt;&lt;br /&gt;Third concept: the 3 sided polygon, a.k.a the Triangle.  Triangles are useful things for 3D graphics, because they are &lt;a href="http://en.wikipedia.org/wiki/Coplanar"&gt;coplanar&lt;/a&gt;.  Safe to say, everything we build we'll make out of triangles in 3D space.  All a triangle consists of is three Points.  From those Points, you can work out what the edges look like, and represent them as Vectors, and with those Vectors you can work out a cross product to get a Vector that points perpendicular to the triangle.&lt;br /&gt;&lt;br /&gt;Fourth and final concept: Coordinate systems.  A coordinate system simply means that you agree on the numbers you're going to use to define your x,y,z values.  When it comes to 3D graphics, you're usually concerned with at least two coordinate systems.  &lt;span style="font-style: italic;"&gt;World coordinates&lt;/span&gt; tell you where the object is in absolute space.  If the object is not moving, its world coordinates remain the same.  But if you're moving the camera around it, the object moves on screen, so it must be moving in &lt;span style="font-style: italic;"&gt;some &lt;/span&gt;coordinate system.  That's your &lt;span style="font-style: italic;"&gt;view coordinates - &lt;/span&gt;the position of the object in relation to your eye.  If your eye (the camera) moves, the object moves in your view coordinates&lt;span style="font-style: italic;"&gt;.  &lt;/span&gt;Generally, the two are equal and opposite.  Let's say I'm looking at the front face of a cube, and I want to see the right hand side of it.  I could do two things to achieve the same effect.  I could stay still and turn the cube 90 degrees clockwise, which would involve changing it's world coordinates.  Or I could leave the cube alone and move myself 90 degrees anti-clockwise, which would be a change in my view coordinates.  Either way, the result with regards to the cube is the same.  In the following examples, our viewpoint will stay the same, and we'll just rotate the object in space.  If we wanted, we could achieve the same thing by keeping the same world coordinates and moving the camera instead.&lt;br /&gt;&lt;br /&gt;Right, let's get cracking on some code.  We're working in Java, so first thing we're going to want is a JFrame and a JPanel to draw our scene on.  This is pretty standard stuff.  Standard practice is to override the JComponent's paintComponent() method, call super.paintComponent(), and then do our business. We'll use one little trick here.  We'll generally be creating stuff centred around the origin, which is at point (0,0,0).  The problem is that as far as the Java2D libraries are concerned, the point (0,0) is the top left of the screen, so everything we do will be squeezed up in that corner.  We could load our equations involving coordinates with some kind of offset to push it into the middle of the screen, but the Graphics2D object offers us an easier solution, the &lt;a href="http://java.sun.com/j2se/1.4.2/docs/api/java/awt/geom/AffineTransform.html"&gt;AffineTransform&lt;/a&gt;.  The Javadoc looks a little scary, but ultimately it's just a way to tell Java2D to automatically do the offset for us without having to think about it in the calculations.  It's made even easier by helper methods such as translate() which mean you don't even have to get your hands dirty in matrix maths.  So if we specify &lt;span style="font-family:courier new;"&gt;graphics.translate(panel.getWidth()/2, panel.getHeight()/2)&lt;/span&gt;, then anything that we draw at (0,0) will be in the middle of the panel, and not the top left.&lt;br /&gt;&lt;br /&gt;We'll have to draw something, otherwise we'll just get a black screen, so let's plot some Points.  That gives us an opportunity to write a class to model our first concept, the Point.  As we already said, it just has three coordinates, so those are just instance members of our class.  Let's not bother with getters and setters, it's just unnecessary bloat, and those equations are going to look pretty ugly otherwise.  This isn't Enterprise Code now, you know.&lt;br /&gt;&lt;br /&gt;I'm also going to define an interface, Drawable, with a single method &lt;span style="font-family:courier new;"&gt;draw(Graphics2D)&lt;/span&gt; which any objects, be they Points or Triangles or whatever, will implement.  In the case of the Point, we're just going to draw that point at the x,y coordinates, and ignore the z component.  You might call it lazy, I call it &lt;a href="http://en.wikipedia.org/wiki/Orthographic_projection"&gt;Parallel Projection&lt;/a&gt;.  We'll store all the objects to be drawn in an ArrayList of Drawables, and then the panel just has to iterate that list and ask each object in turn to draw itself.&lt;br /&gt;&lt;br /&gt;I think we're ready to go - &lt;a href="http://www.revbingo.f2s.com/blogfiles/PartI.zip"&gt;download the source&lt;/a&gt;. So, we bung all this together, and what do you get?&lt;br /&gt;&lt;br /&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/_U7NJax4HHlo/SLlYRXbL7FI/AAAAAAAABjY/j2z1c1_mFJ4/s1600-h/PartI.png"&gt;&lt;img style="cursor: pointer;" src="http://4.bp.blogspot.com/_U7NJax4HHlo/SLlYRXbL7FI/AAAAAAAABjY/j2z1c1_mFJ4/s320/PartI.png" alt="" id="BLOGGER_PHOTO_ID_5240316696791346258" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;Yup, that's, errr, impressive alright.  Still, we've got the basic code in place, let's head to &lt;a href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-ii-round-we-go.html"&gt;Part II - Round We Go&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-429800808627761618?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/429800808627761618/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=429800808627761618' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/429800808627761618'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/429800808627761618'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-i-basics.html' title='Adventures in 3D: Part I - The Basics'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://4.bp.blogspot.com/_U7NJax4HHlo/SLlYRXbL7FI/AAAAAAAABjY/j2z1c1_mFJ4/s72-c/PartI.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-1892761693762706031</id><published>2008-08-30T05:45:00.000-07:00</published><updated>2008-08-31T14:18:19.407-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='3D'/><category scheme='http://www.blogger.com/atom/ns#' term='Java'/><title type='text'>Adventures in 3D: Intro</title><content type='html'>Always on the lookout for something to mess around with codewise, I settled on the idea of venturing into the world of 3D graphics.  In the days of Blitz Basic on the Amiga, school lessons in trigonometry inspired me to get as far as a spinning wireframe cube, but I've never really delved into the Java graphics libraries in any great depth.&lt;br /&gt;&lt;br /&gt;Of course, Java has a fully featured and ready made &lt;a href="http://java.sun.com/javase/technologies/desktop/java3d/"&gt;3D library&lt;/a&gt;, but that's no fun at all!  I'm the sort of guy who likes to understand the nuts and bolts before moving on to the shortcuts, so I decided to have a go at everything from first principles (read: maths).&lt;br /&gt;&lt;br /&gt;Just a couple of days into it, I've got some fairly simple yet fairly spiffy stuff going on, so it struck me as something worth putting back into the bin of knowledge, and blogging it may just concrete some of the concepts into my head a bit more.  There's plenty of other reading material out there, so I'm not going to dwell too much on the mathematics, but hopefully it might give you a foot up to get you started.&lt;br /&gt;&lt;br /&gt;A disclaimer though - I'm learning this as I go too, so I make no guarantees that this is necessarily the right or best or fastest way to do things.  It's more an exercise in understanding the concepts rather than trying to write the most elegant/fastest/shortest code.&lt;br /&gt;&lt;br /&gt;Let's start at &lt;a href="http://revbingo.blogspot.com/2008/08/adventures-in-3d-part-i-basics.html"&gt;Part I - The Basics&lt;/a&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-1892761693762706031?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/1892761693762706031/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=1892761693762706031' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/1892761693762706031'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/1892761693762706031'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/08/adventures-in-3d-intro.html' title='Adventures in 3D: Intro'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-5120836202436048337</id><published>2008-08-30T00:56:00.000-07:00</published><updated>2008-08-30T05:41:39.279-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Music'/><category scheme='http://www.blogger.com/atom/ns#' term='R.E.M.'/><title type='text'>Pretty Persuaded</title><content type='html'>Question: What is the greatest piece of silence ever captured on record?  Answer: Forget John Cage and his lengthy &lt;span style="font-style: italic;"&gt;4'33"&lt;/span&gt;.  It's that brief pause on R.E.M.'s &lt;span style="font-style: italic;"&gt;Lifes Rich Pageant&lt;/span&gt; that is bookended with the sharp departure of &lt;span style="font-style: italic;"&gt;Begin The Begin&lt;/span&gt; and the jangling, diving intro to &lt;span style="font-style: italic;"&gt;These Days&lt;/span&gt;.  It's so utterly imprinted on my brain that to have one song without the other, and that magical non-audible glue in the middle, is like Morecambe without Wise.&lt;br /&gt;&lt;br /&gt;I bring this up for no good reason other than because I was reminded of it this week when it failed to materialise during the band's performance at the Southampton Rose Bowl. However, as a whole, it rocked.  I won't go on at length about my teenage obsession with the band (the Out Of Time, AFTP era), but safe to say that as the years have gone on, they've lost a certain relevance, and certainly the last time I saw them live (the Up tour), it was a disappointment.&lt;br /&gt;&lt;br /&gt;No disappointment this time though, utterly brilliant. Sure, bit too many "greatest hits" for my liking, but also a decent smattering of the good stuff, and the new songs from &lt;span style="font-style: italic;"&gt;Accelerate &lt;/span&gt;are great on the CD, and even better live.&lt;br /&gt;&lt;br /&gt;The set list (in album order, it's the only way I can remember these things)&lt;br /&gt;&lt;br /&gt;Pretty Persuasion&lt;br /&gt;7 Chinese Bros&lt;br /&gt;Auctioneer (Another Engine)&lt;br /&gt;Begin The Begin  &lt;span style="font-style: italic;"&gt; (not followed by These Days though, boooo)&lt;/span&gt;&lt;br /&gt;Fall On Me&lt;br /&gt;It's The End Of The World As We Know It (And I Feel Fine)&lt;br /&gt;The One I Love&lt;br /&gt;Orange Crush&lt;br /&gt;Losing My Religion&lt;br /&gt;Drive&lt;br /&gt;Ignoreland &lt;span style="font-style: italic;"&gt;  (a definite highlight)&lt;/span&gt;&lt;br /&gt;Man On The Moon&lt;br /&gt;Nightswimming&lt;br /&gt;What's The Frequency, Kenneth?&lt;br /&gt;Let Me In  &lt;span style="font-style: italic;"&gt;(Mike, Scott McCaughey and Bill Rieflin on acoustic guitars, all crowded around Peter on keyboard)&lt;/span&gt;&lt;br /&gt;Electrolite&lt;br /&gt;Imitation of Life&lt;span style="font-style: italic;"&gt;&lt;span style="font-style: italic;"&gt;&lt;span style="font-style: italic;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="font-style: italic;"&gt;&lt;span style="font-style: italic;"&gt;&lt;/span&gt;&lt;/span&gt;Living Well Is The Best Revenge&lt;br /&gt;Man-Sized Wreath&lt;br /&gt;Hollow Man&lt;br /&gt;Supernatural Superserious&lt;br /&gt;Horse To Water&lt;br /&gt;I'm Gonna DJ&lt;span style="font-style: italic;"&gt;&lt;br /&gt;&lt;/span&gt;The Great Beyond&lt;br /&gt;Animal&lt;br /&gt;&lt;span style="font-style: italic;"&gt;&lt;br /&gt;&lt;/span&gt;Nothing from Chronic Town or Murmur, nothing from Up, save Michael's acapella version of the first two lines from 'Hope' whilst Mike prepared himself to play Nightswimming, which was a shame - I like Up.  Also nothing from Around The Sun - no-one missed it.&lt;br /&gt;&lt;br /&gt;Consider R.E.M. back on the playlist.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-5120836202436048337?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/5120836202436048337/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=5120836202436048337' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/5120836202436048337'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/5120836202436048337'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/08/pretty-persuaded.html' title='Pretty Persuaded'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-8650707047494955404</id><published>2008-06-26T12:23:00.000-07:00</published><updated>2008-06-26T12:33:32.843-07:00</updated><title type='text'>The Mysterious Case of the Disappearing Downloads</title><content type='html'>One for the internet knowledge bin.  For a little while now, Firefox (both 2.0 and the freshly installed 3.0) has been forgetting my downloads.  That is, the files are downloaded to my chosen location, but as soon as they're done, they disappear from both the usual Firefox downloads window, and the neat (and very highly recommended) &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/26"&gt;Download Statusbar&lt;/a&gt; add-on.&lt;br /&gt;&lt;br /&gt;I checked the immediate culprit - the Privacy tab in the preferences has a tick box labelled "Remember what I've downloaded".  But that was ticked, so it can't be that.  I checked it wasn't the fault of other plugins.  Not that either.  So I kind of resigned myself to having to skip to the desktop to retrieve my downloads, promising that next time I'd try and figure it out properly.&lt;br /&gt;&lt;br /&gt;Anyways, tonight my patience ran out. And it turns out to be very simple - that tick box &lt;span style="font-weight:bold;"&gt;looked&lt;/span&gt; ticked, but given that when I clicked on it first time it stayed ticked, it suggests that really Firefox considered it to not be set. &lt;br /&gt;&lt;br /&gt;In summary, for those not paying attention, give it a few clicks to get it in sync, and wa la, downloads back to normal again. Easy.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-8650707047494955404?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/8650707047494955404/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=8650707047494955404' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/8650707047494955404'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/8650707047494955404'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/06/mysterious-case-of-disappearing.html' title='The Mysterious Case of the Disappearing Downloads'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-1926779737980963896</id><published>2008-04-07T08:38:00.000-07:00</published><updated>2008-04-07T11:22:43.467-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='knowledgebin'/><category scheme='http://www.blogger.com/atom/ns#' term='firefox'/><category scheme='http://www.blogger.com/atom/ns#' term='Linux'/><title type='text'>I was blind, but now I see</title><content type='html'>We're now a while into life with Hardy x86_64, and so far so good.  However, it's not been entirely without hiccup.&lt;br /&gt;&lt;br /&gt;The decision to include Firefox 3 as standard was a brave but perhaps foolhardy move.  A sizeable enough number of extensions are not compatible with FF3 - although I'm pleased that &lt;a href="http://www.foxmarks.com/"&gt;Foxmarks&lt;/a&gt; is now available, at least if you sign up to the beta.  My usual mouse gestures extension isn't compatible, although I found a replacement.  Unfortunately enough, that replacement (FireGestures) was good for 3b4, but not 3b5, which has just been pushed down the pipes, which means that currently I'm having to do without mouse gestures.  For me it's no great shakes, as I only have one or two that I use, but if you're a heavy user, it's going to be a problem.&lt;br /&gt;&lt;br /&gt;The other issue I've had in Firefox3 is with scaled images.  Any images that used height and width tags to scale the image would appear as just a black rectangle - viewing the image full size would work.  The issue is described in &lt;a href="https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/182038"&gt;bug #182038&lt;/a&gt;.  In summary, it seems there are three possible fixes: xulrunner1.9 has a fix that may or may not work for you; otherwise, try editing your /etc/X11/xorg.conf file and in the "Device" section adding:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;Option "AccelMethod" "EXA"&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;And if &lt;b&gt;that&lt;/b&gt; doesn't work (and for me, with SiS onboard graphics, it crashed X), you can try instead adding:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;Option "XAANoOffscreenPixmaps" "true"&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;I've no idea what this does or if it might cause any other problems, but it did the business for me.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-1926779737980963896?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/1926779737980963896/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=1926779737980963896' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/1926779737980963896'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/1926779737980963896'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/04/i-was-blind-but-now-i-see.html' title='I was blind, but now I see'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-3912451893550515004</id><published>2008-04-03T03:31:00.000-07:00</published><updated>2008-04-10T10:09:15.351-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='knowledgebin'/><category scheme='http://www.blogger.com/atom/ns#' term='XSLT'/><title type='text'>Ah, that feels good...</title><content type='html'>Is there any better feeling than finally finding an elegant answer to a problem that's previously forced you to use dirty hacks?  The freedom that comes from getting to strip out line after line of filthy code and replace it with a single line that does the job properly.&lt;br /&gt;&lt;br /&gt;XSLT is my latest flirtation.  I've used it a bit before, but I've come back to it to solve a couple of problems at work, and the more I use it, the more powerful and elegant it becomes.  In my line of work, we do an awful lot of XML processing, especially converting to CSV, and up until now it's largely been writing and rewriting SAX handlers to do the job.  This has been the default option because, well, someone's already done the hard work before, so the quickest route to get something done is to copy another SAX handler, tweak it, and job done.  But it's slow and difficult to maintain, even with the best written code (and believe me,  a lot of our SAX handlers are far from it).  XSLT is really what should have been used from the very start.&lt;br /&gt;&lt;br /&gt;Anyway, that problem.  We use a third party product as our core system, and a large part of what we do is to take reports from that system as XML, and turn them into something that a lowly user could make sense of (read: CSV).  I really want to push the use of XSLT to do this, and set about doing some proof of concept stuff.  This was all going very well, until I came across a set of reports that appear to use some sort of generic MS database export - the namespace declaration is:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&amp;lt;xml xmlns:s='uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882'&lt;br /&gt;xmlns:dt='uuid:C2F41010-65B3-11d1-A29F-00AA00C14882'&lt;br /&gt;xmlns:rs='urn:schemas-microsoft-com:rowset'&lt;br /&gt;xmlns:z='#RowsetSchema'&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;The sharp eyed amongst you will notice that the declaration of the z namespace involves a &lt;span style="font-style: italic;"&gt;relative&lt;/span&gt; path to an embedded schema,which is a &lt;a href="http://www.w3.org/TR/REC-xml-names/#iri-use"&gt;deprecated practice&lt;/a&gt;.  This is no bad thing, but given this xml fragment:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&amp;lt;z:row someAttr="someData" /&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;and this stylesheet fragment:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&amp;lt;xsl:template match="z:row"&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;&lt;a href="http://xml.apache.org/xalan-j/"&gt;Xalan&lt;/a&gt; appears to throw a bit of a wobbly:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;A node test that matches either NCName:* or QName was expected&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;It seems this is Xalan specific - other XSLT processors handle it just fine.  So, the obvious solution is to switch processors right?  Well, maybe, but dammit, I use Xalan for other stuff, I'm not prepared to let this stand in the way.  Removing the # from the namespace declaration in both the stylesheet and the xml makes it work.&lt;br /&gt;&lt;br /&gt;I spent a while looking at this, and finally came up with a dirty old hack - the use of a HashFilterInputStream.  That's right, it's that dirty, a FilterInputStream that filters out the hash, so the resulting namespace is simply "RowsetSchema".  Dirty, but it worked, as long as you weren't expecting a hash sign to occur anywhere else in the document.  Believe me, I had sleepless nights over this one.&lt;br /&gt;&lt;br /&gt;Thankfully, on another pass over the problem, I finally found a much more agreeable solution. Make no mistake, it's still a bit of a hack and no replacement for Xalan actually getting fixed, but at the very least it meant I didn't have to rely on my Java code to do the right thing before processing the transform.  The solution is this - where you reference a tag in the dodgy namespace, explicitly test for the node-name() and namespace-uri() as strings, rather than letting Xalan do the name resolution.&lt;br /&gt;&lt;br /&gt;So where you may have previously used:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&amp;lt;xsl:template match="z:row"&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;you should instead do the explicit test:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&amp;lt;xsl:template match="node-name()='row' and namespace-uri()='#RowsetSchema'"&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;You're welcome.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-3912451893550515004?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/3912451893550515004/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=3912451893550515004' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/3912451893550515004'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/3912451893550515004'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/04/ah-that-feels-good.html' title='Ah, that feels good...'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-7089860680277063937</id><published>2008-03-30T14:08:00.000-07:00</published><updated>2008-03-30T14:48:49.768-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Linux'/><title type='text'>Ubuntu uh-oh</title><content type='html'>Uh oh, it's happened again.  Upgrade fever strikes.  You see, my laptop is an AMD64, and it struck me that I was running a 32 bit OS.  I've flirted with 64 bit before with XP 64, and the benefit just didn't justify the  hassle of getting things working.  That said, it also struck me that if I was ever going to install the 64 bit version, it would be an awful lot easier to do it now, whilst I was still in fairly virgin territory, than a year down the line when reinstalling everything would be a PITA.&lt;br /&gt;&lt;br /&gt;So, deep breath, quick read of the &lt;a href="http://wwww.ubuntuforums.org/"&gt;Ubuntu forums&lt;/a&gt; to make sure I wasn't diving in to a complete mess, and off we go with downloading the ISO - it's times like this that an employer's huge bandwidth and a 1Gb USB stick come in useful.&lt;br /&gt;&lt;br /&gt;Nothing to report from the install step (apart from the DUH moment of taking 20 minutes to figure out why GParted is complaining that I haven't selected any root partitions), and soon we're off and running.  My old friend, the wireless, is back to haunt me, but I'm a veteran now, and it's less than 30 minutes before it's working using the Restricted Driver Manager.&lt;br /&gt;&lt;br /&gt;The most remarkable thing about the 64 bit version is that it's pretty unremarkable.  Apart from the &lt;a href="http://ubuntuforums.org/showpost.php?p=2863873&amp;amp;postcount=1"&gt;well documented&lt;/a&gt; lack of Flash for 64bit Firefox - which is easily remedied - everything works as you might expect. There's no great shakes as far as performance goes, at least not in day-to-day use, but I get a warm fuzzy feeling knowing that those extra 32 bits in my CPU, which until now have been sitting idle, waiting for the day when they'd be called into action, are now busy helping out.&lt;br /&gt;&lt;br /&gt;I promise that I'm at the end of upgrades and installations now.  Not least because the other half is getting a bit bored of finding everything has changed yet again.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-7089860680277063937?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/7089860680277063937/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=7089860680277063937' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/7089860680277063937'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/7089860680277063937'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/03/ubuntu-uh-oh.html' title='Ubuntu uh-oh'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-2545208345834401551</id><published>2008-03-22T04:55:00.000-07:00</published><updated>2008-03-23T07:50:36.781-07:00</updated><title type='text'>Ubuntu upgrade</title><content type='html'>No sooner is Ubuntu up and running than I spy that the &lt;a href="http://www.ubuntulinux.org/testing/hardy/beta"&gt;beta of Hardy Heron&lt;/a&gt; is available.  I've finally got a nice stable system, I've found all the bits and pieces I need, I've customised it just nicely.  There's no point in upgrading for the sake of it, is there?  Errm, well yes actually, there is - because I can! &lt;br /&gt;&lt;br /&gt;Thankfully, Ubuntu's update manager works pretty seamlessly - select your updates, click install, away it goes.  For anyone not familiar with Linux, installation of software is (by default, at least) from centralised Ubuntu package servers, which means a) no hunting round the net for compatible downloads, and b) a damn big download pipe.  It's one of the few places on the net where I actually get some use out the bandwidth I pay for.  For Hardy, the total download was around 780MB, taking a mere 20 minutes.  It wasn't all that long ago when I'd wait that long for a single MP3. The installation itself was pretty straightforward too - should the distribution be about to overwrite config files that you may have changed manually, it'll pop up a window allowing you to do a diff and check out the differences, then either accept one of the versions or try and do a merge.  An hour or so later, Hardy is installed and ready to go.&lt;br /&gt;&lt;br /&gt;Admittedly there's nothing incredibly exciting about Hardy, the most noticeable change for me being the inclusion of Firefox 3 beta 4 as standard.  This is a pretty bold move by Ubuntu - unfortunate in that a significant number of my add-ons (Foxmarks, Gmail Manager, All-in-One gestures) aren't yet compatible.  However, Hardy does also bring with it a new Screen Resolution app, which may go some way to assuaging &lt;a href="http://revbingo.blogspot.com/2008/03/ubuntu-underway.html"&gt;my complaints&lt;/a&gt; about getting the basics right.  It's nice to know that the Ubuntu community is taking my opinions seriously.&lt;br /&gt;&lt;br /&gt;The worst sin commited was the sudden non-workiness of the good old wireless card, and the fact that the ndiswrapper solution that did the job last time failed to revive it.  With a bit more poking around, I resorted to retrying the kernel's b43 restricted driver and &lt;code&gt;b43-fwcutter&lt;/code&gt; package, which also didn't work until I downloaded the updated &lt;a href="http://linuxwireless.org/en/users/Drivers/b43"&gt;4.80 firmware&lt;/a&gt; from linuxwireless.org.  Suddenly, a flash of light and the wireless card is on.  Hooray!  Unfortunately though, still no actual sign of a network connection, with &lt;code&gt;ifup&lt;/code&gt; reporting errors whenever I tried to bring up eth1.  In a wireless-less mire, and sick of going over the same web pages, I was about to give up when a random webpage pointed me at using &lt;code&gt;dmesg&lt;/code&gt; to examine the kernel ring buffer.  This threw up a whole lot of interesting information, showing that the wireless card was being seen but also being disabled.  It also included a helpful line that pointed out that there was still a need to actually press the button on the front of the laptop to enable the wireless.  Despite the light already being on, I thought there was no harm in it - and whaddya know, wireless, pure sweet digital-air goodness.  As an added bonus, it also appears to be rock-steady, certainly a lot more stable than it was under Gutsy (or Windows for that matter).&lt;br /&gt;&lt;br /&gt;Today's last deposit into the bin of knowledge - another thing that stopped working was my configuration for tapping the corners of the touchpad to go back/forward when browsing.  I had the synaptics driver set up (in &lt;code&gt;/etc/X11/xorg.conf&lt;/code&gt;) to send events 6 and 7 from those taps, which worked with Gutsy.  The simple, but ultimately confusing, solution under Hardy was to change this to events 8 and 9.  Ours is not to reason why, but it works.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-2545208345834401551?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/2545208345834401551/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=2545208345834401551' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/2545208345834401551'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/2545208345834401551'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/03/ubuntu-upgrade.html' title='Ubuntu upgrade'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-8890739873376835770</id><published>2008-03-19T14:35:00.000-07:00</published><updated>2008-03-19T15:52:40.801-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='i-bloody-tunes'/><category scheme='http://www.blogger.com/atom/ns#' term='Linux'/><title type='text'>Ubuntu upwards</title><content type='html'>I could bang on about getting a wireless card with a Broadcom 4318 chipset in an Acer Aspire going under Ubuntu, but frankly it's a well covered subject already to which I can lend little insight (hint - you need &lt;a href="http://ubuntuforums.org/attachment.php?attachmentid=48087&amp;amp;d=1193544558"&gt;this&lt;/a&gt; [Ubuntu forum login required] and &lt;a href="http://code.google.com/p/acer-acpi-deb/"&gt;this&lt;/a&gt;, and I had to modify /etc/network/interfaces to add a gateway route to 192.168.1.1, my router, when eth1 is brought up).  Besides, if I'm really honest, it was largely luck rather than judgment that did the trick.&lt;br /&gt;&lt;br /&gt;It's been a week or so now since I booted Windows - or am I legally obliged to call it Windoze now? - and I'm not sure I miss it. If I have to score points, the consistent use of oversize fonts by Linux applications irks a little bit, it makes me feel like I'm missing out on screen real estate. Like I said before, I like things dinky. But at least most of them let you change that, just a shame the defaults are a bit clunky. Anyway, I've gone for MgOpen Modata 8 as a system wide font, which compensates slightly and I'm pretty happy with how it looks.   The subpixel rendering is nice, Cleartype has never looked good on my laptop screen.  Combine with "Glider" controls theme and the Whiteglass pointer scheme (still not sure why my "insert text" pointer has to be so damn thick - just give me 1 pixel wide dammit!), and all in all it's looking pretty sexy, sexier than Windows ever did anyway.&lt;br /&gt;&lt;br /&gt;Talking of fonts, one application that doesn't play ball is Amarok, but I suspect that's because it's a KDE app doing it's best to keep up in a GNOME environment.  Regardless of that small quirk, Amarok is really quite nifty as a music library organiser and iPod syncer.  On Windows, iTunes was just horrible - slow, freeze-prone, sometimes hard to fathom.  Oh, and "Gapless Playback Analyser"?  Stop that.  No really, stop it.  Just when you think iTunes is finally getting it together, the bloody Gapless Playback Analyser kicks in and suddenly every mouse click takes 20 seconds.  I suspect it works great if you've got 200 files on your local disk, but with 8000 songs on a NAS, it's no fun.  On the other hand, you could use Windows Media Player - but if you want to sync your iPod, you'll have to shell out for &lt;a href="http://www.mgtek.com/dopisp/"&gt;dopisp&lt;/a&gt;, and even then it doesn't support podcasts.  I'm sure there's probably other software out there, but I never found it.   Well, now I've found Amarok, and it does a good job.  No fuss, things just make sense, and it doesn't bork my ipod, which is a bonus.  Plus it kindly went away and grabbed a whole ton of album covers, which is nice.&lt;br /&gt;&lt;br /&gt;Let's finish with some music - I have &lt;a href="http://www.garageband.com/artist/the_plenty"&gt;Plenty&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;[Edit:  That Amarok font problem - sorted by installing kcontrol, which basically works for all KDE apps.  Amarok now looks as sexy as everything else.  Consider me pleased]&lt;br /&gt;&lt;br /&gt;&lt;div style="text-align: center;"&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_U7NJax4HHlo/R-GZA7zMdZI/AAAAAAAABi4/TVFI3SRC8SM/s1600-h/Screenshot-Amarok.png"&gt;&lt;img style="cursor: pointer;" src="http://3.bp.blogspot.com/_U7NJax4HHlo/R-GZA7zMdZI/AAAAAAAABi4/TVFI3SRC8SM/s320/Screenshot-Amarok.png" alt="" id="BLOGGER_PHOTO_ID_5179589287784248722" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-8890739873376835770?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/8890739873376835770/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=8890739873376835770' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/8890739873376835770'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/8890739873376835770'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/03/ubuntu-upwards.html' title='Ubuntu upwards'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/_U7NJax4HHlo/R-GZA7zMdZI/AAAAAAAABi4/TVFI3SRC8SM/s72-c/Screenshot-Amarok.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-112262088742052760</id><published>2008-03-16T14:09:00.000-07:00</published><updated>2008-03-16T14:52:46.342-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='l33t h4x0r'/><category scheme='http://www.blogger.com/atom/ns#' term='Linux'/><title type='text'>Ubuntu underway</title><content type='html'>Hey, a second post!   Bonus.&lt;br /&gt;&lt;br /&gt;This week has seen my reintroduction to Linux.  My previous incursion into such territory was having Mandrake installed on an old desktop, and it wasn't a wholly pleasant experience.  Not wholly unpleasant, but not enough to convert me.  The desktop went a couple of years ago to be replaced by a laptop, and I haven't seen fit to reinstall a Linux distro since.   It was just a bit too much work, a little bit slow and clunky. But, in a fit of adventurism, assisted by the absence of wife and child, I dug out an Ubuntu live CD I downloaded months ago with good intentions, and decided to give it a whirl.  Heretoforth springs my considered thoughts on the subject.&lt;br /&gt;&lt;br /&gt;Ubuntu wants to project an image of being easy to use - "Ubuntu just works".  No better test of such a thing than to get it installed.  I have a certain fascination for the concept of the live CD, which will let your average Joe Punter try out a distro without having to install anything.  If only more software would let you do that before it stamps it's size 9's all over your lovingly ordered hard disk and sticks it's chocolate covered fingers  in your registry. So you try the live CD, and when you've tried it, and liked it (hopefully), you hit the button that says "Install" and away you go.  Alarmingly, this is exactly what I did with Ubuntu, and it really just worked.  Admittedly repartioning your disks isn't something your gran would want to do, but the tools were there to do the job without fuss for anyone of reasonably sound mind.  Also admittedly, I was rather hoping it would just work, because being a cowboy I didn't bother with the tiresome business of backing up my data.  I like that little frisson when the progress bar stalls and the screen blinks - it's sky-diving for nerds.  End result, 20 minutes later, the machine reboots, Grub pops up, and we're off.  Handshakes and whiskies all round.&lt;br /&gt;&lt;br /&gt;The Ubuntu desktop is instantly familiar if you've ever been near a Linux distro before.  On the downside, it's in 1024x768, which may as well be 320x200 for all I'm concerned.  I like dinky.  My laptop can manage 1280x800, and damn it that's what I want. But I can forgive Ubuntu for not knowing that, and there's a handy Screen and Graphics item in the System menu, so it's a quick change.  Oh, apart from it's not - there's no option for 1280x800.  Suddenly memories of the same thing on Mandrake come flooding back, and my enthusiasm for Linux is slightly dimmed.  It's slightly tempting to say that if Linux is to make it as a mainstream desktop OS, this sort of thing has to work out the box. But who are we kidding?  The whole point of Linux is that it isn't a mainstream desktop OS, it's an OS for nerds.  If your gran started using Debian, it would kind of defy the point.&lt;br /&gt;&lt;br /&gt;So let's just admit that here - you use Linux because it's not straightforward.  When you see that your choice of resolution isn't in the list, a small trickle of excitement ensues.  And when you finally trawl Google and get to type &lt;code&gt;sudo dpkgs-reconfigure -phigh xserver-xorg&lt;/code&gt; and press Enter to accept the defaults on lots of obscure options, you are a l33t h4x0r.   Of course, this all has it's limits.  No-one wants to spend 5 hours on this stuff.  That's reserved for the wireless card, and is, of course, a whole other story...&lt;br /&gt;&lt;br /&gt;(to be continued...)&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-112262088742052760?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/112262088742052760/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=112262088742052760' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/112262088742052760'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/112262088742052760'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/03/ubuntu-underway.html' title='Ubuntu underway'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2353779988231261722.post-2548165029264361546</id><published>2008-03-16T06:34:00.000-07:00</published><updated>2008-03-16T13:18:25.418-07:00</updated><title type='text'>A New Hope</title><content type='html'>So look, here's the deal.  Yes, it's another random, spectacularly normal, person setting up a blog.  No, it's probably not going to get updated very often.  Yes, there's a reasonable chance that you'll come back in a year and this will still be the only post on here.  No, I'm not expecting anyone to read it.  All of which sounds like a pretty bad basis on which to start a blog, but allow me to explain.&lt;br /&gt;&lt;br /&gt;There's a &lt;a href="http://en.wikipedia.org/wiki/The_Long_Tail#Chris_Anderson_and_Clay_Shirky"&gt;long tail&lt;/a&gt; of blogs out there that were founded on exactly that basis.  I've never done the numbers, but I suspect - no, I &lt;b&gt;know&lt;/b&gt; - that for every regularly updated, well written, widely read blog, there are 100 that were a wet Sunday afternoon diversion and now sit abandoned, a Christmas puppy that finds its way to the &lt;a href="http://www.dogshome.org/"&gt;dog's home&lt;/a&gt; before New Year's Eve.&lt;br /&gt;&lt;br /&gt;The thing is, like those puppies, just because they're in the dog's home, doesn't mean they're useless. Blogs mean different things to different people.  It's true that there's an awful lot of mundane cruft out there, but blogs, even those abandoned, are also realising the potential of the internet to be a big bin of knowledge.  Blogs sweep up that knowledge that isn't worth knowing, or at least not in the collective sense. No-one needs a wikipedia article about &lt;a href="http://madbean.com/2004/mb2004-20/"&gt;StackOverflowError in java.util.regex.Pattern&lt;/a&gt;, or &lt;a href="http://bechblog.blogspot.com/2006/08/bea-81-servlet-container.html"&gt;quirks in Weblogic's servlet implementation&lt;/a&gt; but when you come across that problem, it sure is useful information. &lt;br /&gt;&lt;br /&gt;And that leads me here.  I've taken an awful lot from other people - fixes, workarounds, walkthroughs, or just plain advice - and it seems to me that maybe I should get off my butt and put something back in that bin of knowledge. Sure, it might just all go to waste, but perhaps one day, someone will be trawling the 85th page of their Google search on some weird error they've seen, and my blog will give them just a hint on how they might solve it.  Job done.&lt;br /&gt;&lt;br /&gt;As a side effect, it's also just a useful exercise in writing. I don't do enough of it and I should do more. You never know, I might even improve with time.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2353779988231261722-2548165029264361546?l=revbingo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://revbingo.blogspot.com/feeds/2548165029264361546/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2353779988231261722&amp;postID=2548165029264361546' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/2548165029264361546'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2353779988231261722/posts/default/2548165029264361546'/><link rel='alternate' type='text/html' href='http://revbingo.blogspot.com/2008/03/new-hope.html' title='A New Hope'/><author><name>Mark Piper</name><uri>http://www.blogger.com/profile/12108942153819990692</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry></feed>
