Letters to the Editor

Quite a few readers responded to last month's JDK 1.2 bug report with skepticism at best; author Kieron Murphy takes on the critics. Plus: VolanoMark's John Neffenger addresses a mailbag full of JVM scalability queries; Merlin Hughes tackles the mathematical intricacies of 3D programming in Java; Allen Holub defends his approach to coding; and technical Q&A with Chuck McManis, Todd Sundsted, Bill Venners, and John Zukowski

1 2 3 4 Page 3
Page 3 of 4
  1. Produce a background image. You'll have to hack the code to not perform perspective transformation and return a blue-black fade: Terrain:
     
                                           RGB blue = new RGB (0.0, 0.0, 1.0);
                                           public RGB getColor (double i, double j) {
                                           double a = getAltitude (i, j);
                                           return blue.scale (a);
                                           }
                                          
    Landscape:
      void eyeToScreen () {
                                           int w = getSize ().width, h = getSize ().height;
                                           for (int i = 0; i <= steps; ++ i) {
                                           for (int j = 0; j <= steps; ++ j) {
                                           Triple p = map[i][j];
                                           double x = p.x, y = p.y, z = p.z;
                                           scrMap[i][j] = new XY ((int) (x * w), (int) (z * h));
                                           }
                                           }
                                           }
                                          
    Rasterizer:
      public void savePPM (String file) throws IOException {
                                           FileOutputStream fo = new FileOutputStream (file);
                                           DataOutputStream dO = new DataOutputStream (fo);
                                           dO.write ("P6\n".getBytes ());
                                           dO.write ("# written by merlin\n".getBytes ());
                                           dO.write ((width + " " + height + "\n").getBytes ());
                                           dO.write ("255\n".getBytes ());
                                           for (int i = 0; i < width * height; ++ i) {
                                           int rgb = buffer[i];
                                           dO.write (rgb >> 16);
                                           dO.write (rgb >> 8);
                                           dO.write (rgb >> 0);
                                           }
                                           dO.close ();
                                           }
                                          
    The last fragment saves a PPM-format image. You may also want to disable shadows.
  2. Load the background image into the rasterizer. Rasterizer:
      int[] bg;
                                           public Rasteriser (int width, int height) {
                                           this.width = width;
                                           this.height = height;
                                           buffer = new int[width * height];
                                           bg = new int[width * height];
                                           zBuffer = new double[width * height];
                                           try {
                                           byte[] b = new byte[3];
                                           DataInputStream d = new DataInputStream (new FileInputStream ("bg.ppm"));
                                           d.readLine ();
                                           d.readLine ();
                                           d.readLine ();
                                           d.readLine ();
                                           for (int i = 0; i < width * height; ++ i) {
                                           d.readFully (b, 0, 3);
                                           bg[i] = (0xff << 24) | (((b[1] & 0xff) / 4) << 16) |
                                           (((b[1] & 0xff) / 2) << 8) | ((b[2] & 0xff) / 2);
                                           }
                                           d.close ();
                                           } catch (IOException ex) {
                                           System.err.println (ex);
                                           }
                                           clear ();
                                           }
                                           public void clear () {
                                           for (int i = 0; i < width * height; ++ i) {
                                           buffer[i] = bg[i];
                                           zBuffer[i] = Double.MAX_VALUE;
                                           }
                                           }
                                          
  3. Make a new fractal terrain to produce the landscape.
      double sea;
                                           public FractalTerrain (..., int seed) {
                                           ...
                                           rng = new Random (seed);
                                           terrain[0][0] = 
                                           terrain[0][divisions] = 
                                           terrain[divisions][divisions] = 
                                           terrain[divisions][0] = rnd ();
                                           ...
                                           sea = min + (max - min) * .5;
                                           }
                                           public double getAAltitude (double i, double j) {
                                           double alt = terrain[(int) (i * divisions)][(int) (j * divisions)];
                                           return (alt < sea) ? (alt - min) / (sea - min) * 0.1 : (alt - sea) / (max - sea) * 0.9 + 0.1;
                                           }
                                           public double getAltitude (double i, double j) {
                                           double alt = terrain[(int) (i * divisions)][(int) (j * divisions)];
                                           double b = (alt < sea) ? (alt - min) / (sea - min) * 0.1 : (alt - sea) / (max - sea) * 0.9 + 0.1;
                                           return (b < .5)  ? .25 + b * b : b;
                                           }
                                           // Ignore the names!!
                                           RGB blue = new RGB (0.0, 0.2, 0.0);
                                           RGB yellow = new RGB (0.4, 0.2, 0.0);
                                           RGB green = new RGB (0.6, 0.24, 0.0);
                                           RGB grey = new RGB (1.0, 0.4, 0.1);
                                           RGB white = new RGB (1.0, 1.0, 1.0);
                                           public RGB getColor (double i, double j) {
                                           double a = getAAltitude (i, j) * (0.8 + 0.2 * Math.sin (i * 7.0) * Math.cos (j * 7.0));
                                           if (a < .1) {
                                           return blue.add (yellow.subtract (blue).scale ((a - 0.0) / 0.1));
                                           } else if (a < .4) {
                                           return yellow.add (green.subtract (yellow).scale ((a - 0.1) / 0.3));
                                           } else if (a < .7) {
                                           return green.add (grey.subtract (green).scale ((a - 0.4) / 0.3));
                                           } else {
                                           return grey.add (white.subtract (grey).scale ((a - 0.7) / 0.3));
                                           }
                                           }
                                          
    The first difference you see here is that I specify a seed and construct the RNG with that seed. Then I just initialize the four corners to the same value. Keep in mind that this is old code, so it may need to be changed to fit in with the new code, but these changes should be obvious. The second difference is that I applied a wavy sea level and made the altitude-to-color transformation a bit less banded. I can't justify this particular approach; it's just what I had when I produced that image. The seed value I used for that image is 311. I used a roughness of .4 and vertical exaggeration of .7. This code should produce a reasonable image. (I also added a filter to remove spikes; a bit messy.)
  4. To add the planet, run the modified terrain, but produce a green-blue fade. Convert this to a JPEG and load it into the first article in the series. Finally, write an image compositor and mix them to create the background.
  5. Adding a second light is a lot of effort. I recommend that you, instead, soften the shadows. You can quickly do this by changing shade[?][?] = 0.5 (instead of 0.0).

Try that out and see what results you get. The image I created was at a grid resolution of 1<<8, but 1<<6 will produce a reasonably smooth result. If it's still not right, mail me your modified code and I'll give it a better look. Merlin Hughes

Image animation

Merlin,

Your 3D Graphics series is "Very worth reading!" Your explanation of difficult topics like algorithmic textures, supersampling, and spherical coordinate systems is both helpful and funny.

I'm hoping you can provide me with more explanation (and maybe some source code) about java.awt.image classes. It's easy to put an image on the screen with Java, but adding animation (like waving or rotating objects) is a little bit difficult. Can you help?

Bruno Salzano

Bruno, The

java.awt.image

framework just isn't fast enough for most animation purposes. The only way to animate images at a reasonable speed is to generate a bunch of

Image

objects, use a

MediaTracker

to get them ready for drawing, and then display them one after the other, like this:

int N = 16;
                                       Image image;
                                       Image[] images = new Image[N];
                                       In a subthread:
                                       // fill in images[0..N-1]
                                       // use a MediaTracker to ready the images
                                       int index = 0;
                                       while (true) {
                                       image = images[index];
                                       repaint ();
                                       Thread.sleep (100);
                                       index = (index + 1) % N;
                                       }
                                       public void update (Graphics g) { paint (g); }
                                       public void paint (Graphics g) {
                                       if (image != null) g.drawImage (image, 0, 0, this);
                                       }
                                      

I am planning to write a column on 3D animation later this year (or early next year); until then, this is about all I can suggest. Merlin Hughes

"Programming Java threads in the real world" by Allen Holub

Read

Coding conventions -- a personal matter?

Allen,

My name is Laurence Vanhelsuwé, and I'm a JavaWorld contributor like yourself.

I read your first article on multithreading issues with interest. Its conclusion was extremely surprising and enlightening!

I was also very pleased to read that you consider C++ as a bad dream (same here).

On this topic, I've got a "religious issue" that I'd like to bring up: your code examples use C++-style identifier naming. In case you didn't know, Java "comes with" its own naming and coding convention which differs significantly from its C++ equivalents. This convention is documented in the Java Language Specification.

In an ideal world, I'd love to see every Java book/article author stick to the JLS code/naming conventions. Such consistent adherence to a Java language-tailored convention really does help to relegate C++ to "bad dream" status, rather than something which manages to stay with us in all kinds of counter-productive ways (for example, via C++-style identifiers in Java programs).

Your opinions on this would be appreciated.

In the mean time, I look forward to reading more of your articles on multithreading in Java.

Laurence Vanhelsuwé

P.S. FYI, several existing Java tools complain when you throw Java classes at them that do not adhere to JLS identifier naming conventions.

Laurence, We're going to have to agree to disagree on this one. I'm a strong believer in maximum readability of code. I am inflexible in following a personal rule: all nontrivial variable names be real words in English (that you can look up in a dictionary). This rule often makes the code self documenting, and makes it vastly easier for a non-native English speaker to maintain. To enforce this rule, I always run my code through a spell checker, and my spell checker treats underscores as punctuation. If I were to adapt the Pascal-style naming conventions favored by Sun, then none of my variable names would spell check properly. (I should also say that using underscores is not a C++ vs. Java issue. Pascal used mixed upper/lower case only because underscore wasn't permitted in an identifier. I asked Wirth about this once, and he said that it was an inadvertent commission from the language. Many C++ programs use Pascal-style names, many don't.) The conventions that are important are upper-case first letters on classes, lower-case first letters on methods and fields, all lower case in packages, and all upper case for constants. I follow these rules religiously. Since Sun consistently uses Pascal-style naming conventions, it's also easy for me to distinguish my method names from Sun names in derived classes. If I see MixedUpperAndLower, I know it must be defined in a Sun base class, not one of mine. Allen Holub

Java In Depth: "The basics of Java class loaders" by Chuck McManis

Read

Customizing custom class loaders

Chuck,

I am trying to develop a couple of classes to load jar files dynamically. I think the best way to perform this task is to create a custom class loader.

Your article states:

CustomClassLoader
                                       ccl = new CustomClassLoader();
                                       Object o;
                                       Class c;
                                       c = ccl.loadClass("someNewClass");
                                       o = c.newInstance();
                                       ((SomeNewClass)o).someClassMethod();
                                      

However, you cannot cast o to someNewClass because only the custom class loader "knows" about the new class it has just loaded.

There are two reasons for this. First, the classes in the Java virtual machine are considered castable if they have at least one common class pointer. However, classes loaded by two different class loaders will have two different class pointers and no classes in common (except java.lang.Object usually). Second, the idea behind having a custom class loader is to load classes after the application is deployed so the application does not know a priori about the classes it will load. This dilemma is solved by giving both the application and the loaded class a class in common.

In my opinion, this restriction invalidates the solution of using a class loader to load jar files dynamically, because files included in a jar will be very different and cannot extend a common class or implement a common interface. I need to refer to specific methods of classes and not to generic methods of a common extended class.

How can I avoid this restriction? Do you have any solution? Perhaps I didn't understand the main idea about the cast in a custom class loader. Is there a site where I can find more docs on this theme?

Chemi

Chemi, A couple of things:

  1. For the first handoff, the initial class loaded must share an interface that was loaded by both the custom class loader and the primordial (internal) class loader.
  2. When a class is loaded by the custom class loader, any classes it loads are loaded by the custom class loader and don't have this restriction.

So, you write a class loader that loads classes dynamically from jar files. You then write an "app loader" that shares an interface with the internal class loader that you have defined. Consider the following interface:

public interface AppLoader {
                                       public void main(String args[]);
                                       }
                                       Now you write an application loader like so:
                                       public MyAppLoader implements AppLoader {
                                       /* implements the interface */
                                       public void main(String args[]) {
                                       Constructors cc[];
                                       if (args.length == 0)
                                       return;
                                       Class c = Class.classForName(args[0]);
                                       cc = c.getDeclaredConstructors();
                                       cc[0].invoke( ... );
                                       }
                                       }
                                      

Call your custom class loader on the applet loader, then cast the result to an instance of

AppLoader

, invoke its main method with the name of the class you want to run and the constructor to use, etc. That class will never know it was loaded by a custom class loader (until it tries to do something involving security -- but that's a different question.) The code described is called a

trampoline

and it creates a class loader context, then loads a helper class that loads the target class. Chuck McManis

How-To Java: "Using the Graphics class" by Todd Sundsted

Read

If it's arrow heads you want...

Todd,

I'm having problems drawing lines with arrow heads. Do you have any idea how should I go about doing it ?

Peggy

Peggy, Unfortunately, the Graphics class supports only the most basic drawing operations. If you want arrow heads, you'll have to draw them yourself. To solve problems like this in the past (and to avoid having to reinvent code with each new application I write) I've created graphics helper classes. These helper classes hold methods for drawing advanced graphical objects. I'd suggest creating a class with methods for drawing lines with arrow heads of various sizes and shapes. To my knowledge, no one has done arrow heads yet. It might make a nice package of code to publish on the Net. Todd Sundsted

I can't pass my applet!

Todd,

Do you know how to pass Image objects obtained in an applet via the getImage() method to "public" helper classes -- i.e., a class that draws a custom-made ImageButton -- without getting an IllegalAccessError in Netscape?

I can draw my image on my applet, I just can't pass it to my ImageButton class. Any thoughts?

Ellen Lay

Ellen, An llegalAccessError is thrown by the Java virtual machine when a method of a class tries to access a field or method of another class that it doesn't have access to (because it's private, perhaps). This is usually caught by the compiler. Sometimes it isn't. Here's an example: If a method in class A calls a public method in class B, then class A will compile. If you then change class B and make the method private, the access violation won't be caught until runtime -- whereupon the VM will throw an IllegalAccessError. I would suspect you're compiling with one compiler (javac from the JDK, perhaps) and running under Netscape. Each of these products uses its own class files. There is probably some incompatibility between the two -- and that's causing the error. I can't provide any suggestions as to what the problem is, but I'd begin tracking it down by running the applet in the appletviewer and in another browser. See what (if anything) changes. I suspect it will run fine in the appletviewer (because it uses the same class files as the compiler). Todd Sundsted

Under The Hood: "How the Java virtual machine handles method invocation and return " by Bill Venners

Read

Direct (interface) references

Bill,

I have a problem understanding one concept in your article. In the "The invokeinterface instruction" section, you state:

The invokeinterface opcode performs the same function as invokevirtual. The only difference is that invokeinterface is used when the reference is of an interface type.

Does this mean the reference of an interface is an instance of a class that implements a particular interface?

If this is correct, why can't the JVM have the same index for a method declared in an interface? In class references, the JVM knows about the index of a method in a method table. I'd like to know why this differs for interface references.

Shailesh Shah

Shailesh, Any class can implement an interface, and the methods that implement the interface can appear anywhere in the class declaration. This in turn means that the method pointers for the interface methods can appear anywhere in the method table for a class. Thus, with only a reference of the interface type, the JVM can't be sure where in the method table the method pointer for a particular method will show up. See my book Inside the Java Virtual Machine (Chapter 8 "The Linking Model" in the section called "Direct References") for a nice example to your question. Bill Venners

Design Techniques: "Design for thread safety" by Bill Venners

Read

Synchronized methods -- faster than you think

Your article is a good introduction for people new to multithreaded systems. A couple of comments:

Related:
1 2 3 4 Page 3
Page 3 of 4