import java.awt.*; import java.applet.*; import java.awt.image.*; /** * Test applet for using a triangular underlying mesh instead * of a square one */ public class Trid extends Applet { final int COARSE_MESH_DIM = 1; final int MAX_LEVELS = 9; final int SIDE_DIM = COARSE_MESH_DIM * ( 1 << MAX_LEVELS ); final double alpha = 1.0; public boolean generateDone = false; public boolean useBadInterpolation = false; // public boolean useBadInterpolation = true; public boolean debugTiming = true; public boolean useMIS = true; public final static int SHADE_HEIGHTMAP = 0; public final static int SHADE_SURFACE_NORMAL = 1; public int shadingMode = SHADE_SURFACE_NORMAL; // public int shadingMode = SHADE_HEIGHTMAP; public float [][] z; public float [][] zshade; public float [][][] norm; public boolean [][] zset; public int [][] iz; public void init() { generateHierarchicalFunction(); Thread animator = new Thread() { public void run() { while ( true ) { animateCycle(); try { Thread.currentThread().sleep( 100 ); } catch ( InterruptedException ie ) { } } } }; animator.start(); } public float randomFloat() { return ( float ) Math.random(); } static float [] ztmp = new float[ 7 ]; public void computeSurfaceNormal( int i, int j ) { // fill in the array with the surrounding points if // available or z[i][j] if not boolean allZSet = true; for ( int k = 1; k < 7; k++ ) ztmp[ k ] = z[ i ][ j ]; if ( j > 0 ) { ztmp[ 1 ] = z[ i ][ j - 1 ]; if ( !zset[ i ][ j - 1 ] ) allZSet = false; } if ( j < SIDE_DIM - i ) { ztmp[ 4 ] = z[ i ][ j + 1 ]; if ( !zset[ i ][ j + 1 ] ) allZSet = false; } if ( i > 0 ) { if ( j > 0 ) { ztmp[ 6 ] = z[ i - 1 ][ j - 1 ]; if ( !zset[ i - 1 ][ j - 1 ] ) allZSet = false; } if ( j < SIDE_DIM - ( i - 1 ) ) { ztmp[ 5 ] = z[ i - 1 ][ j ]; if ( !zset[ i - 1 ][ j ] ) allZSet = false; } } if ( i < SIDE_DIM ) { if ( j + 1 < SIDE_DIM - ( i + 1 ) ) { ztmp[ 3 ] = z[ i + 1 ][ j + 1 ]; if ( !zset[ i + 1 ][ j + 1 ] ) allZSet = false; } if ( j < SIDE_DIM - ( i + 1 ) ) { ztmp[ 2 ] = z[ i + 1 ][ j ]; if ( !zset[ i + 1 ][ j ] ) allZSet = false; } } float zavg = ( ztmp[ 1 ] + ztmp[ 2 ] + ztmp[ 3 ] + ztmp[ 4 ] + ztmp[ 5 ] + ztmp[ 6 ] ) / 6; for ( int k = 1; k <= 6; k++ ) ztmp[ k ] -= zavg; float bb = ( 2.0f * ( ztmp[ 4 ] - ztmp[ 1 ] ) + ztmp[ 3 ] + ztmp[ 5 ] - ztmp[ 2 ] - ztmp[ 6 ] ) / 8; float cc = ( ztmp[ 2 ] + ztmp[ 3 ] - ztmp[ 5 ] - ztmp[ 6 ] ) / 4; float b = 2.0f * bb; float c = sq3o2i * cc; float normalize = ( float ) Math.sqrt( 1.0 + b * b + c * c ); norm[ i ][ j ][ 0 ] = - c / normalize; norm[ i ][ j ][ 1 ] = - b / normalize; norm[ i ][ j ][ 2 ] = 1.0f / normalize; if ( !allZSet ) { norm[ i ][ j ][ 0 ] = 0; norm[ i ][ j ][ 1 ] = 0; norm[ i ][ j ][ 2 ] = 0; } } public float computeSurfaceNormalDotLight( int i, int j ) { return norm[ i ][ j ][ 0 ] * xLight + norm[ i ][ j ][ 1 ] * yLight + norm[ i ][ j ][ 2 ] * zLight; } final static float sq3o2i = ( float ) ( 2.0 / Math.sqrt( 3.0 ) ); float xLightAngle = ( float ) ( 0.4 * Math.PI ); float yLightAngle = ( float ) ( 0.3 * Math.PI ); float xLight = ( float ) ( Math.cos( xLightAngle ) * Math.sin( yLightAngle ) ); float yLight = ( float ) ( Math.sin( xLightAngle ) * Math.sin( yLightAngle ) ); float zLight = ( float ) Math.cos( yLightAngle ); // interp_weight is the relative weight of the nearer vertices // compared to the farther vertices. There are several mathematically // justifiable choices for interp_weight: // // Inf this is equivalent to useBadInterpolation = true; the far // vertices aren't weighted at all, which produces really // extreme mesh artifacts // // 0 this uses ONLY the far vertices, and while it produces almost // no grid artifacts it makes a REALLY weird picture. The // height map is really gentle. // // 1 if we do the interpolation by least-squares fit to a plane, // z(x,y) = A + B x + C y, it produces this choice. It doesn't // produce a mesh-orientation-independent picture, especially // for alpha >= 2, and I don't know why. Seems like it should. // // 9 if instead of a plane we fit a biquadratic with no x-y cross // term (which means grid-orientation-dependence!) we get this // weight. It works ok, but there's definite grid dependence at // large values of alpha // // sqrt(3) this one isn't mathematically justified but it seems to work // pretty well. 2 and 3 also work reasonably well, producing only // the "nipples" effect at alpha >~= 2. // // Is the nipple effect necessarily bad? If the soil has a constant // angle of repose, then alpha should really be around 1. It's really // more complicated than this; I need to think about it. alpha = 2 is // probably way too big. A realistic calculation would be alpha = 1 // with some sort of "maximum angle of repose" smoothing happening // at each step. float interp_weight = 2.0f; float interp_weightx2 = interp_weight * 2; public void generateHierarchicalFunction() { // float base_height = 1.0f; // float height = base_height / 2; long start_time = System.currentTimeMillis(); float base_height = 0; float height = 1.0f; double base_angle = Math.PI / 6.0; height = ( float ) ( Math.tan( base_angle ) * ( 1 << MAX_LEVELS ) ); // fill in the top-level random function on // the triangular coarse mesh int stride = 1 << MAX_LEVELS; z = new float[ SIDE_DIM + 1 ][]; norm = new float[ SIDE_DIM + 1 ][][]; iz = new int[ SIDE_DIM + 1][]; zset = new boolean[ SIDE_DIM + 1 ][]; if ( shadingMode == SHADE_SURFACE_NORMAL ) { zshade = new float[ SIDE_DIM + 1 ][]; } for ( int i = 0; i <= SIDE_DIM; i++ ) { z[ i ] = new float[ SIDE_DIM - i + 1 ]; norm[ i ] = new float[ SIDE_DIM - i + 1 ][ 3 ]; zset[ i ] = new boolean[ SIDE_DIM - i + 1 ]; iz[ i ] = new int[ SIDE_DIM - i + 1 ]; if ( shadingMode == SHADE_SURFACE_NORMAL ) { zshade[ i ] = new float[ SIDE_DIM - i + 1 ]; } for ( int j = 0; j <= SIDE_DIM - i; j++ ) { zset[ i ][ j ] = false; } } for ( int i = 0; i <= SIDE_DIM; i += stride ) { for ( int j = 0; j <= SIDE_DIM - i; j += stride ) { z[ i ][ j ] = base_height * ( randomFloat() - 0.5f ); zset[ i ][ j ] = true; } } // perform MAX_LEVELS passes of interpolate / perturb over the mesh for ( int l = MAX_LEVELS; l > 0; l-- ) { stride = 1 << l; int half_stride = stride / 2; for ( int i = half_stride; i <= SIDE_DIM; i += stride ) { for ( int j = 0; j <= SIDE_DIM; j += stride ) { // pass 1 interp: x = j, y = i, interp from (x, y +- half) // (Good: interp from (x +- 1, y) and (x, y +- half) ) if ( i + half_stride <= SIDE_DIM - j ) { z[ j ][ i ] = ( z[ j ][ i - half_stride ] + z[ j ][ i + half_stride ] ) * interp_weight; float npts = interp_weightx2; if ( !useBadInterpolation ) { if ( ( j > 0 ) && ( i + half_stride <= SIDE_DIM - ( j - stride ) ) ) { z[ j ][ i ] += z[ j - stride ][ i + half_stride ]; npts++; } if ( ( j + stride <= SIDE_DIM ) && ( i - half_stride <= SIDE_DIM - ( j + stride ) ) ) { z[ j ][ i ] += z[ j + stride ][ i - half_stride ]; npts++; } } z[ j ][ i ] /= npts; z[ j ][ i ] += height * ( randomFloat() - 0.5f ); zset[ j ][ i ] = true; } // pass 2 interp: y = j, x = i, interp from (x +- half, y) if ( j <= SIDE_DIM - ( i + half_stride ) ) { z[ i ][ j ] = ( z[ i - half_stride ][ j ] + z[ i + half_stride ][ j ] ) * interp_weight; float npts = interp_weightx2; if ( !useBadInterpolation ) { if ( ( i + half_stride <= SIDE_DIM ) && ( i - half_stride + j <= SIDE_DIM ) && ( j >= stride ) ) { z[ i ][ j ] += z[ i + half_stride ][ j - stride ]; npts++; } if ( ( i > 0 ) && ( i + j + half_stride <= SIDE_DIM ) ) { z[ i ][ j ] += z[ i - half_stride ][ j + stride ]; npts++; } } z[ i ][ j ] /= npts; z[ i ][ j ] += height * ( randomFloat() - 0.5f ); zset[ i ][ j ] = true; } // pass 3 interp: z = j, x = i, interp from (x +- half, z) // where y = DIM - x - z int k = SIDE_DIM - i - j; int k1 = k - half_stride; int k2 = k + half_stride; if ( ( k1 >= 0 ) && ( k2 <= SIDE_DIM - i ) ) { z[ i ][ k ] = ( z[ i - half_stride ][ k2 ] + z[ i + half_stride ][ k1 ] ) * interp_weight; float npts = interp_weightx2; if ( !useBadInterpolation ) { if ( ( i >= half_stride ) && ( i - half_stride + k1 <= SIDE_DIM ) ) { z[ i ][ k ] += z[ i - half_stride ][ k1 ]; npts++; } if ( ( i + half_stride <= SIDE_DIM ) && ( i + half_stride + k2 <= SIDE_DIM ) && ( k2 >= 0 ) ) { z[ i ][ k ] += z[ i + half_stride ][ k2 ]; npts++; } } z[ i ][ k ] /= npts; z[ i ][ k ] += height * ( randomFloat() - 0.5f ); zset[ i ][ k ] = true; } } } height *= ( float ) Math.pow( 2.0, -alpha ); } long hf_end_time = System.currentTimeMillis(); if ( shadingMode == SHADE_SURFACE_NORMAL ) { for ( int i = 0; i <= SIDE_DIM; i++ ) { for ( int j = 0; j <= SIDE_DIM - i; j++ ) { computeSurfaceNormal( i, j ); } } } long normal_end_time = System.currentTimeMillis(); if ( debugTiming ) { System.out.println( "HF Gen took " + ( 0.001 * ( hf_end_time - start_time ) ) + " s" ); System.out.println( "Normals took " + ( 0.001 * ( normal_end_time - hf_end_time ) ) + " s" ); } generateDone = true; repaint(); } public void reshade() { if ( shadingMode == SHADE_SURFACE_NORMAL ) { for ( int i = 0; i <= SIDE_DIM; i++ ) { for ( int j = 0; j <= SIDE_DIM - i; j++ ) { zshade[ i ][ j ] = computeSurfaceNormalDotLight( i, j ); if ( zshade[ i ][ j ] < 0 ) { zshade[ i ][ j ] = 0; } } } } } double theta = 0.0; public void animateCycle() { theta += 0.02 * Math.PI; if ( theta > 2.0 * Math.PI ) theta -= 2.0 * Math.PI; xLightAngle = ( float ) Math.cos( theta ); yLightAngle = ( float ) Math.sin( theta ); /* xLightAngle += ( float ) ( 0.02 * Math.PI ); if ( xLightAngle > Math.PI - 0.1 ) { xLightAngle = 0.1f; } yLightAngle += ( float ) ( 0.01 * Math.PI ); if ( yLightAngle > Math.PI - 0.1 ) { yLightAngle = 0.1f; } */ xLight = ( float ) xLightAngle; yLight = ( float ) yLightAngle; zLight = ( float ) 1.0f; float norm = ( float ) Math.sqrt( xLight * xLight + yLight * yLight + 1.0 ); xLight /= norm; yLight /= norm; zLight /= norm; reshade(); repaint(); } boolean colorsDone = false; Color [] colors = new Color[ 100 ]; float zmin, zmax; Image buffer = null; int [] pix = new int[ ( SIDE_DIM + 1 ) * ( SIDE_DIM + 1 ) ]; boolean repaintInProgress = false; MemoryImageSource mis = null; Image misbuf = null; public void paint( Graphics gg ) { if ( repaintInProgress ) { return; } repaintInProgress = true; if ( generateDone ) { if ( !colorsDone ) { long color_start_time = System.currentTimeMillis(); zmax = -1000; zmin = 1000; for ( int i = 0; i <= SIDE_DIM; i++ ) { for ( int j = 0; j <= SIDE_DIM - i; j++ ) { if ( shadingMode == SHADE_SURFACE_NORMAL ) { if ( zshade[ i ][ j ] > zmax ) { zmax = zshade[ i ][ j ]; } if ( zshade[ i ][ j ] < zmin ) { zmin = zshade[ i ][ j ]; } } else { if ( z[ i ][ j ] > zmax ) { zmax = z[ i ][ j ]; } if ( z[ i ][ j ] < zmin ) { zmin = z[ i ][ j ]; } } } } for ( int i = 0; i < 100; i++ ) { colors[ i ] = new Color( 2 * i, 2 * i, 2 * i ); } long color_end_time = System.currentTimeMillis(); if ( debugTiming ) { System.out.println( "Color gen took " + ( 0.001 * ( color_end_time - color_start_time ) ) + " s" ); } colorsDone = true; } if ( buffer == null ) { Dimension dim = getSize(); buffer = createImage( dim.width, dim.height ); } Graphics g = buffer.getGraphics(); // draw it int scale = 1 + ( 400 / ( SIDE_DIM + 1 ) ); // int ypad = 100; int ypad = 10; int eff_height = ( SIDE_DIM + 1 ) * scale + ypad; // g.setColor( new Color( 128, 128, 255 ) ); g.setColor( Color.black ); g.fillRect( 0, 0, eff_height, eff_height ); long draw_start_time = System.currentTimeMillis(); float ndz = 99 / ( zmax - zmin ); int bluepix = ( 255 << 24 ) + ( 128 << 16 ) + ( 128 << 8 ) + 255; bluepix = 0; for ( int i = 0; i < pix.length; i++ ) pix[ i ] = bluepix; int ct = 0; int mask = ( 1 << 16 ) + ( 1 << 8 ) + 1; for ( int i = 0; i <= SIDE_DIM; i++ ) { ct += i / 2; for ( int j = 0; j <= SIDE_DIM - i; j++ ) { if ( shadingMode == SHADE_SURFACE_NORMAL ) { iz[ i ][ j ] = ( int ) ( ndz * ( zshade[ i ][ j ] - zmin ) ); } else { iz[ i ][ j ] = ( int ) ( ndz * ( z[ i ][ j ] - zmin ) ); } pix[ ct++ ] = ( 2 * iz[ i ][ j ] * mask ) + ( 255 << 24 ); } ct += i - i / 2; } long draw_mid_time = System.currentTimeMillis(); if ( ( useMIS ) && ( scale == 1 ) ) { if ( mis == null ) { mis = new MemoryImageSource( SIDE_DIM + 1, SIDE_DIM + 1, pix, 0, SIDE_DIM + 1 ); mis.setAnimated( true ); misbuf = createImage( mis ); } else { mis.newPixels( 0, 0, SIDE_DIM + 1, SIDE_DIM + 1 ); } g.drawImage( misbuf, 0, ypad, null ); } else { for ( int c = 0; c < 100; c++ ) { g.setColor( colors[ c ] ); int ys = eff_height; for ( int i = 0; i <= SIDE_DIM; i++ ) { for ( int j = 0; j <= SIDE_DIM - i; j++ ) { if ( ( zset[ i ][ j ] ) && ( iz[ i ][ j ] == c ) ) { int xs = scale * ( j + i / 2 ); g.fillRect( xs, ys - scale, scale, scale ); } } ys -= scale; } } } long draw_end_time = System.currentTimeMillis(); if ( debugTiming ) { System.out.println( "iz calc took " + ( 0.001 * ( draw_mid_time - draw_start_time ) ) + " s" ); System.out.println( "Draw took " + ( 0.001 * ( draw_end_time - draw_mid_time ) ) + " s" ); System.out.println( " scale = " + scale ); } gg.drawImage( buffer, 0, 0, null ); } repaintInProgress = false; } public void update( Graphics g ) { paint( g ); } }