6

I am working on a 3D renderer in Java using the Graphics class, It is now capable of drawing any shape with coloured faces, however I was wondering if it was possible to texture the faces? I have seen a lot of people creating software renderers in Javascript so surely there is an equivalent function/method of however they are doing it in Java...
I have looked around so far but all I can find is Graphics.setClip(Shape), I don't think it would be suitable because it merely sets the background texture and wouldn't stretch the texture if a vertex moved - and that's just in 2D, it need to also stretch/skew the texture when it's at an angle to the camera (Think of the sides of a rotating cube).

I really have no idea where to start, I can't use XOR modes because of no skewing and I really wouldn't know how to do the math if I had to do it manually.
How do these Javascript software renderers do it so well?

Lee Fogg
  • 775
  • 6
  • 22

3 Answers3

7

You may be able to leverage java.awt.TexturePaint, illustrated here and here. In this context, you should know that TexturePaint aligns with the rendering surface's raster, rather than the shape's bounds.

image

Addendum: While shading is a broad topic, also consider a pixel-based approach using gradient shading with alpha, illustrated in the KineticModel cited here. Note that such a gradient can be applied to the WritableRaster of a TexturePaint.

For non-affine transformations, see javax.media.jai.Warp.Warp, cited here.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
5

I looked at doing this as a "fallback" for an OpenGL feature that would not run on some machines due to JOGL problems. I was unsuccessful. These were the unresolved questions that caused me to stop work:

  • Hidden surface removal. I could find no way to implement a z-buffer under Graphics2d primitives.

  • Perspective texture transforms. The AffineTransform available in Graphics2d has enough power to map textures if the image projection is parallel, but not perspective.

  • Mismatches in 2d clipping. The last operation in texturing will have to be clipping against a 2d mask. It turns out there is a bug in Graphics2d clipping. If you clip against perfectly adjoining 2d polygons, the clipped patches do not mate perfectly. Single pixels at the boundary are unshaded.

  • Performance. While recent versions of the Graphics2d pipeline try to use hardware acceleration if it's there, raw polygon rendering still tested an order of magnitude slower than JOGL, which was not good enough for my purposes.

The Javascript 3d libraries I've looked at are all built on WebGL, which in turn is implemented in HTML 5 Canvas objects. WebGL is an OpenGL API that must be implemented by the browser. Other 3d Javascript libraries use plug-ins to get at hardware-accelerated graphics. So they're not a useful source of information about how to do 3d in Swing.

Addition

Perhaps it is worth adding what I did do. To replace the 3d JOGL scene that the user can "fly through" by moving the camera, I chose a single, fixed viewpoint and "hard wired" the drawing order, effectively implementing the Painter's Algorithm with fixed logic to determine sort order, rendering more-or-less the same models as the JOGL view. I implemented Gouraud shading using gradient-filled polygons, which is where I found the clipping bug mentioned above. It all works and is running in hundreds of thousands of copies, but it's messy and fragile, and I wouldn't want to do it again.

Gene
  • 46,253
  • 4
  • 58
  • 96
  • Right..How does this answer my question? And have you looked into LWJGL? It works on any machine with OpenGL because its basically a Java to OGL's C++ pipeline patch. I have also found a way to do hidden surface removal using face normals, no Z-buffer needed. – Lee Fogg Feb 24 '14 at 02:03
  • @LeeAllan Culling with face normals only works on one convex object. It's no help when objects occlude each other. Javascript renderers I've seen use the 3d canvas object, which is using OpenGL or similar under the hood. If you give an example that doesn't, I'll look at it. LWJGL has its own problems. – Gene Feb 24 '14 at 03:25
  • 2
    @LeeAllan Sorry I should have added that you're right: I didn't answer your question directly. My intent was to allow you to avoid what will with 99% certainty be a waste of time: the same 3-4 hours of study I spent coming to the conclusions above. Also I added a note on Javascript 3d rendering above. – Gene Feb 24 '14 at 03:44
  • I agree with Gene wholeheartedly. Trying to reinvent the wheel only makes sense if you ain't trying to make the wheel square in the meantime... Using a 2D rendering framework for handling 3D data/rendering with actual 3D viewport is not only a pointless waste of time - it basically forces you to: –  Feb 24 '14 at 14:56
  • a) write a software renderer from scratch (pointless, as you should use e.g. Mesa OpenGL software driver), b) use the renderer to parse and render the data (pointless, since you'll neither attain a sufficient frame rate nor quality hardware renderers or a good SR provide), c) use a 2D framework to display it, resulting in a further threading/efficiency issues (also obviously pointless). tl;dr - you're just unproductively wasting your time. –  Feb 24 '14 at 14:56
  • @vaxquis The point of my renderer is for learning so I wouldn't call it a waste of time, and I am currently getting a very decent framerate (>100fps with ~2000 faces). I'm just asking how others texture a polygon on a 2D renderer..it might be handy for something else too... – Lee Fogg Feb 27 '14 at 20:05
  • excuse my previous harsh words, but if you *really* want to texture a polygon properly, you'll have to use linear algebra - strictly speaking, you'll have to define texture's UV coordinates for each polygon vertex, do the R2->R3 transformation to XYZ space, then you'd have to maintain modelview and transform matrices&multiply by them and then use a proper interpolation algorithm (eg bilinear) to match the final screen XY coordinates, effectively retransforming the space into Z2 - ... any other approach would not give you any real insight into how the texturing is done in serious applications. –  Feb 28 '14 at 22:07
  • @LeeAllan I've seen you've been using OpenGL previously - the basic linear algebra behind the pipeline should be your goal if you want to write a software renderer of any kind. –  Feb 28 '14 at 22:17
2

I'm assuming you're only using the Swing/AWT frameworks for graphics. If this is wrong, please update your question.

If you're using Swing and the Graphics2D class (which is the class swing components use), you're dealing with a 2D framework. That just means that fancy 3D stuff isn't built in - you'll have to implement the transformations yourself (or start grabbing 3D classes to do your work).

So, you're on the right track - you'll have to set the clip first (so it fits your shape), then perform a rotation (so it appears at the correct angle).

That being said, doing basic rotational transformations isn't too difficult. There's a good outline of (basic) rotations here. Of course it gets a bit more complicated when you have rotations not based on only one axis. But as the article explains later on, if you multiply the matrices (Rx)(Ry)(Rz), you can use the resulting matrix to determine your pixel placement.

I created a quick example with a rotation on the Y-axis. Please note I made up a dumb algorithm (Magic Vanishingpoint Technology®) to give a vague illusion of depth. I'm assuming you already have something for that - It's probably more correct.

import java.awt.*;
import java.awt.image.*;
import java.io.*;

import javax.imageio.ImageIO;
import javax.swing.*;

public class Rotation3D extends JPanel{
    Image img;

    BufferedImage rotatedImage;
    final int ROTATION_DEGREES = 70;

    int vanishX = 0;
    int vanishY = 0;
    int vanishZ = -1000;

    public Rotation3D(){

        try {
            //Grabbed an image from the java folder - hopefully your computer has it
            img = ImageIO.read(new File(System.getProperty("java.home") + "/lib/deploy/splash.gif"));
            setPreferredSize(new Dimension(img.getWidth(this) * 2,img.getHeight(this) * 2));

            //Create a buffered image with the appropriate size, and draw the image on it
            BufferedImage shadedImage = new BufferedImage(img.getWidth(this), img.getWidth(this), BufferedImage.TYPE_INT_ARGB);
            shadedImage.getGraphics().drawImage(img, 0, 0, this);
            Raster r = shadedImage.getData();

            //Not really necessary unless you're using Magic Vanishingpoint Technology®
            vanishX = shadedImage.getWidth() /2;
            vanishY = shadedImage.getHeight() /2;

            //Create a Wraster for the transformed image
            WritableRaster wr = r.createCompatibleWritableRaster();

            //Do the transformation
            for(int i = 0; i < shadedImage.getWidth(); i++){
                for(int j = 0; j < shadedImage.getHeight(); j++){
                    //Remapping the pixel based on a matrix rotation
                    int[] result = r.getPixel(i, j, new int[4]);
                    Double radians = Math.toRadians(ROTATION_DEGREES);
                    Double newX, newY, newZ;
                    //newX = ((i-vanishX) * Math.cos(radians)) + vanishX; // places the rotation in the middle of the image
                    // x * cos(θ) + y * 0 + z * sin(θ)
                    newX = i * Math.cos(radians); //places the rotation in the y=0 axis
                    // x * 0 + y * 1 + z * 0
                    newY = j * 1.0;
                    // x * -sin(θ) + y * 0 + z * cos(θ)
                    newZ= i * Math.sin(radians) * -1;

                    //Apply Magic Vanishingpoint Technology®
                    //(Not actually trademarked or correct - just something thrown together)
                    if(newZ < vanishZ){
                        newX = 0.0;
                        newY = 0.0;
                    }else if(newZ < 0){
                        double magicVanish =  newZ / vanishZ;
                        newX += magicVanish * newX;
                        newY += magicVanish * newY;
                    }

                    //Print the pixel if it fits on the screen to the new Raster
                    if(newX > 0 && newX < shadedImage.getWidth() && newY > 0 && newY < shadedImage.getHeight())
                        wr.setPixel(newX.intValue(), newY.intValue(), result);
                }
            }

            //Create an image based on the raster.
            rotatedImage = new BufferedImage(img.getWidth(this), img.getWidth(this), BufferedImage.TYPE_INT_ARGB);
            rotatedImage.setData(wr);

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public void paintComponent(Graphics g){
        super.paintComponent(g);
        g.drawImage(rotatedImage, 0, 0, this);
    }

    public static void main(String[] args){
        final JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new Rotation3D());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}
Nick Rippe
  • 6,465
  • 14
  • 30
  • 1
    I am indeed just using Swing. The code is really interesting and has the potential to draw a 3D primitive however two things that I feel would interfere; Why does the rotation past 180° look/act different than the first 180°? And couldn't it be faster?..Otherwise I don't think it could draw a model with more than..10 faces at more than 5FPS. – Lee Fogg Feb 20 '14 at 00:51
  • Unless you get into using your graphics card for processing, you aren't going to be able get too many FPS (because it'll be running on your cpu). OpenGL actually takes advantage of your graphics card - and that's why it runs faster. As far as the rotation/180° go, it seemed to work fine for me with angles such as 300. You don't get the depth feel because my Magic Vanishingpoint Technology® isn't exactly perfect, but it does do the rotation. – Nick Rippe Feb 21 '14 at 16:40