package edu.oregonstate.cartography.grid.operators;
import edu.oregonstate.cartography.grid.Grid;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
/**
* This operator computes a normal map.
*
* @author Bernie Jenny, Oregon State University
*/
public class NormalMapOperator extends ThreadedGridOperator {
/**
* Red, green or blue color channel.
*/
public enum Channel {
R, G, B
};
/**
* The color channel to store the x component of the normal vector. Default is red.
*/
private Channel xChannel = Channel.R;
/**
* The color channel to store the y component of the normal vector. Default is green.
*/
private Channel yChannel = Channel.G;
/**
* The color channel to store the z component of the normal vector. Default is blue.
*/
private Channel zChannel = Channel.B;
/**
* If true, the x component of the normal vector is multiplied with -1.
*/
private boolean invertX = false;
/**
* If true, the y component of the normal vector is multiplied with -1.
*/
private boolean invertY = false;
/**
* If true, the z component of the normal vector is multiplied with -1.
*/
private boolean invertZ = false;
/**
* Vertical exaggeration applied to terrain before the normal vector is computed.
*/
private float vertExaggeration = 1;
// colored image output
private BufferedImage dstImage;
/**
* Creates a new instance with x normal components on red channel, y on
* green channel, and z on blue channel.
*/
public NormalMapOperator() {
}
/**
* Creates a new instance with configurable channel assignments.
*
* @param xChannel The channel for storing the x component of normals.
* @param yChannel The channel for storing the y component of normals.
* @param zChannel The channel for storing the z component of normals.
* @param invertX True if the x component of the normal is to be inverted.
* @param invertY True if the y component of the normal is to be inverted.
* @param invertZ True if the z component of the normal is to be inverted.
* @param vertExaggeration Vertical exaggeration factor for terrain.
*/
public NormalMapOperator(Channel xChannel, Channel yChannel, Channel zChannel,
boolean invertX, boolean invertY, boolean invertZ, float vertExaggeration) {
this.xChannel = xChannel;
this.yChannel = yChannel;
this.zChannel = zChannel;
this.invertX = invertX;
this.invertY = invertY;
this.invertZ = invertZ;
this.vertExaggeration = vertExaggeration;
}
/**
* Do not call this method. It will throw an UnsupportedOperationException.
*
* @param src
* @param dst
* @return
*/
@Override
public Grid operate(Grid src, Grid dst) {
throw new UnsupportedOperationException();
}
/**
* Compute the color image.
*
* @param grid Grid with (elevation) values.
* @param image Image to write pixels to. Can be null.
* @param vertExaggeration Vertical exaggeration factor for terrain.
* @return An image with new pixels. image.
*/
public BufferedImage operate(Grid grid, BufferedImage image, float vertExaggeration) {
dstImage = image;
super.operate(grid, grid);
return dstImage;
}
/**
* Shifts a color value by 16 or 8 bits.
* @param ch The destination color channel.
* @param color The color value to shift.
* @return An integer with the shifted color value.
*/
private int shift(Channel ch, int color) {
if (ch == Channel.R) {
return color << 16;
}
if (ch == Channel.G) {
return color << 8;
}
return color;
}
/**
* Computes a color-coded normal vector.
*
* @param nx X component of the normal vector.
* @param ny Y component of the normal vector.
* @param nz Z component of the normal vector.
* @return ARGB color with encoded normal vector. The alpha component is always 255.
*/
private int normalARGB(double nx, double ny, double nz) {
final double nL = Math.sqrt(nx * nx + ny * ny + nz * nz);
if (Double.isNaN(nL)) {
return 0xFF0000FF; // should this be a vector with 0 length?
}
if (invertX) {
nx *= -1;
}
if (invertY) {
ny *= -1;
}
if (invertZ) {
nz *= -1;
}
final int x = (int) (Math.round((nx / nL + 1d) / 2d * 255d));
final int y = (int) (Math.round((ny / nL + 1d) / 2d * 255d));
final int z = (int) (Math.round((nz / nL + 1d) / 2d * 255d));
return 0xFF000000 | shift(xChannel, x) | shift(yChannel, y) | shift(zChannel, z);
}
/**
* Computes a normal vector and encodes the vector in an ARGB color. Uses 4
* neighboring cells to compute the normal vector.
* @param grid The grid with elevation values.
* @param col Column of cell.
* @param row Row of cell.
* @param nCols Number of columns in grid.
* @param nRows Number of rows in grid.
* @param nz Z component of the normal vector.
* @return ARGB color with encoded normal vector. The alpha component is always 255.
*/
private int normalARGB_4Neighbors(float[][] grid, int col, int row, int nCols, int nRows, double nz) {
if (row == 0) {
// top-left corner
if (col == 0) {
final double s = grid[1][0];
final double e = grid[0][1];
final double c = grid[0][0];
return normalARGB(2 * (e - c), 2 * (s - c), nz);
}
// top-right corner
if (col == nCols - 1) {
final double s = grid[1][nCols - 1];
final double w = grid[0][nCols - 2];
final double c = grid[0][nCols - 1];
return normalARGB(2 * (w - c), 2 * (s - c), nz);
}
// somewhere in top row
final double s = grid[1][col];
final double e = grid[0][col + 1];
final double c = grid[0][col];
final double w = grid[0][col - 1];
return normalARGB(w - e, 2 * (s - c), nz);
}
if (row == nRows - 1) {
// bottom-left corner
if (col == 0) {
final double n = grid[nRows - 2][0];
final double e = grid[nRows - 1][1];
final double c = grid[nRows - 1][0];
return normalARGB(2 * (c - e), 2 * (c - n), nz);
}
// bottom-right corner
if (col == nCols - 1) {
final double n = grid[nRows - 2][nCols - 1];
final double w = grid[nRows - 1][nCols - 2];
final double c = grid[nRows - 1][nCols - 1];
return normalARGB(2 * (w - c), 2 * (c - n), nz);
}
// center of bottom row
final double n = grid[nRows - 2][col];
final double e = grid[nRows - 1][col + 1];
final double c = grid[nRows - 1][col];
final double w = grid[nRows - 1][col - 1];
return normalARGB(w - e, 2 * (c - n), nz);
}
if (col == 0) {
final float[] topR = grid[row - 1];
final float[] ctrR = grid[row];
final float[] btmR = grid[row + 1];
return normalARGB(2 * (ctrR[0] - ctrR[1]), btmR[0] - topR[0], nz);
}
if (col == nCols - 1) {
final float[] topR = grid[row - 1];
final float[] ctrR = grid[row];
final float[] btmR = grid[row + 1];
return normalARGB(2 * (ctrR[nCols - 2] - ctrR[nCols - 1]),
btmR[nCols - 1] - topR[nCols - 1], nz);
}
// normal vector on vertex
final float[] centerRow = grid[row];
final double nx = centerRow[col - 1] - centerRow[col + 1];
final double ny = grid[row + 1][col] - grid[row - 1][col];
return normalARGB(nx, ny, nz);
}
/**
* Compute a chunk of the image.
*
* @param grid Grid with elevation values.
* @param ignore
* @param startRow First row to compute.
* @param endRow First row of next chunk.
*/
@Override
protected void operate(Grid grid, Grid ignore, int startRow, int endRow) {
float[][] g = grid.getGrid();
final int nCols = dstImage.getWidth();
final int nRows = dstImage.getHeight();
final int[] imageBuffer = ((DataBufferInt) (dstImage.getRaster().getDataBuffer())).getData();
// the cell size to calculate the horizontal components of vectors
double cellSize = grid.getCellSize();
// convert degrees to meters on a sphere
if (cellSize < 0.1) {
cellSize = cellSize / 180 * Math.PI * 6371000;
}
// z coordinate of normal vector
double nz = 2 * cellSize / vertExaggeration;
for (int row = startRow; row < endRow; ++row) {
for (int col = 0; col < nCols; ++col) {
imageBuffer[row * nCols + col] = normalARGB_4Neighbors(g, col, row, nCols, nRows, nz);
}
}
}
@Override
public String getName() {
return "Normal Map";
}
}