/*
* MegaMek -
* Copyright (C) 2000,2001,2002,2003,2004,2005,2006 Ben Mazur (bmazur@sev.org)
*
* This file (C) 2008 J�rg Walter <j.walter@syntax-k.de>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package megamek.client.ui.AWT.boardview3d;
import com.sun.j3d.utils.geometry.GeometryInfo;
import com.sun.j3d.utils.geometry.NormalGenerator;
import com.sun.j3d.utils.geometry.Stripifier;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.util.Vector;
import javax.media.j3d.BoundingBox;
import javax.media.j3d.GeometryArray;
import javax.media.j3d.Shape3D;
import javax.vecmath.Point3d;
/**
*
* @author jwalt
*/
class ImageModel extends Shape3D {
private static final int UP = 0;
private static final int RIGHT = 1;
private static final int DOWN = 2;
private static final int LEFT = 3;
private static final int ALPHA_THRESHOLD = 128;
private static final double MODEL_SIZE_CORRECTION = 1.5;
public ImageModel(BufferedImage img, boolean scale) {
super();
GeometryArray geoms[] = tracePixels(img, scale);
for (GeometryArray geom : geoms) {
addGeometry(geom);
}
double p = .5;
if (scale) p *= MODEL_SIZE_CORRECTION;
this.setBounds(new BoundingBox(new Point3d(-p, -p, -p), new Point3d(p, p, p)));
}
private static final GeometryArray[] tracePixels(BufferedImage img, boolean scale) {
WritableRaster r = img.getAlphaRaster();
int w = img.getWidth();
int h = img.getHeight();
int[] pixels = new int[w*h];
r.getPixels(0, 0, w, h, pixels);
boolean[][][] border = new boolean[h+1][w+1][4];
boolean[] set = new boolean[4];
// Pass 1: edge detection, creates an array with orthogonal proto-vectors
for (int y = 0; y <= h; y++) {
for (int x = 0; x <= w; x++) {
set[0] = (x > 0 && y > 0 && pixels[(y-1)*w+(x-1)] > ALPHA_THRESHOLD);
set[1] = (x < w && y > 0 && pixels[(y-1)*w+x] > ALPHA_THRESHOLD);
set[2] = (x > 0 && y < h && pixels[y*w+(x-1)] > ALPHA_THRESHOLD);
set[3] = (x < w && y < h && pixels[y*w+x] > ALPHA_THRESHOLD);
border[y][x][UP] = (!set[0] && set[1]);
border[y][x][RIGHT] = (set[3] && !set[1]);
border[y][x][DOWN] = (!set[3] && set[2]);
border[y][x][LEFT] = (set[0] && !set[2]);
}
}
Vector<Vector<int[]>> contours = new Vector<Vector<int[]>>();
Vector<Integer> scounts = new Vector<Integer>();
int total = 0;
// Pass 2: extract contours, creating diagonal vectors if possible
for (int y = 0; y <= h; y++) {
for (int x = 0; x <= w; x++) {
Vector<int[]> cur, in;
if (border[y][x][UP]) {
int strips = 1;
byte[][] removed = new byte[h+1][w+1];
cur = getContour(border, x, y, UP, removed);
simplify(cur);
// add interiors first to get front and back face right
while ((in = getInterior(border, x, y, removed)) != null) {
simplify(in);
total += in.size();
contours.add(in);
strips++;
}
total += cur.size();
contours.add(cur);
scounts.add(new Integer(strips));
}
}
}
// Pass 3: build geometry arrays
GeometryArray[] geom = new GeometryArray[3];
int ccnt = contours.size();
int[] count = new int[ccnt];
// sides
double[] vertices = new double[total*3*2];
float[] texcoords = new float[total*2*2];
double vw = w*2, vh = h*2, vo = .5;
if (scale) {
vw /= MODEL_SIZE_CORRECTION;
vh /= MODEL_SIZE_CORRECTION;
vo *= MODEL_SIZE_CORRECTION;
}
float tw = w*2, th = h*2;
int voff = 0, toff = 0;
for (int i = 0; i < ccnt; i++) {
Vector<int[]> contour = contours.elementAt(i);
count[i] = contour.size()*2;
for (int j = 0; j < count[i]/2; j++) {
int[] v = contour.elementAt(j);
vertices[voff++] = v[0]/vw-vo;
vertices[voff++] = -v[1]/vh+vo;
vertices[voff++] = -0.5;
vertices[voff++] = v[0]/vw-vo;
vertices[voff++] = -v[1]/vh+vo;
vertices[voff++] = 0.5;
texcoords[toff++] = v[0]/tw;
texcoords[toff++] = 1.0f-v[1]/th;
texcoords[toff++] = v[0]/tw;
texcoords[toff++] = 1.0f-v[1]/th;
}
}
GeometryInfo gi = new GeometryInfo(GeometryInfo.TRIANGLE_STRIP_ARRAY);
gi.setCoordinates(vertices);
gi.setStripCounts(count);
gi.setTextureCoordinateParams(1, 2);
gi.setTextureCoordinates(0, texcoords);
NormalGenerator ng = new NormalGenerator();
ng.generateNormals(gi);
Stripifier st = new Stripifier();
st.stripify(gi);
geom[0] = gi.getGeometryArray();
// top
int[] strips = new int[scounts.size()];
for (int i = 0; i < strips.length; i++) {
strips[i] = scounts.elementAt(i).intValue();
}
count = new int[ccnt];
vertices = new double[total*3];
texcoords = new float[total*2];
float[] normals = new float[total*3];
voff = 0;
toff = 0;
int noff = 0;
for (int i = 0; i < ccnt; i++) {
Vector<int[]> contour = contours.elementAt(i);
count[i] = contour.size();
// reverse path to get front and back face right
for (int j = count[i]-1; j >= 0; j--) {
int[] v = contour.elementAt(j);
vertices[voff++] = v[0]/vw-vo;
vertices[voff++] = -v[1]/vh+vo;
vertices[voff++] = 0.5;
normals[noff++] = 0.0f;
normals[noff++] = 0.0f;
normals[noff++] = 1.0f;
texcoords[toff++] = v[0]/tw;
texcoords[toff++] = 1.0f-v[1]/th;
}
}
gi = new GeometryInfo(GeometryInfo.POLYGON_ARRAY);
gi.setCoordinates(vertices);
gi.setNormals(normals);
gi.setContourCounts(strips);
gi.setStripCounts(count);
gi.setTextureCoordinateParams(1, 2);
gi.setTextureCoordinates(0, texcoords);
st.stripify(gi);
geom[1] = gi.getIndexedGeometryArray(false);
voff = toff = noff = 0;
// bottom
for (int i = 0; i < ccnt; i++) {
Vector<int[]> contour = contours.elementAt(i);
count[i] = contour.size();
// don't reverse path to get front and back face right
for (int j = 0; j < count[i]; j++) {
int[] v = contour.elementAt(j);
vertices[voff++] = v[0]/vw-vo;
vertices[voff++] = -v[1]/vh+vo;
vertices[voff++] = -0.5;
normals[noff++] = 0.0f;
normals[noff++] = 0.0f;
normals[noff++] = -1.0f;
texcoords[toff++] = v[0]/tw;
texcoords[toff++] = 1.0f-v[1]/th;
}
}
gi = new GeometryInfo(GeometryInfo.POLYGON_ARRAY);
gi.setCoordinates(vertices);
gi.setNormals(normals);
gi.setContourCounts(strips);
gi.setStripCounts(count);
gi.setTextureCoordinateParams(1, 2);
gi.setTextureCoordinates(0, texcoords);
st.stripify(gi);
geom[2] = gi.getIndexedGeometryArray(false);
return geom;
}
// join all adjacent colinear segments
private static final void simplify(Vector<int[]> contour) {
contour.removeElementAt(0);
int[] v = contour.elementAt(contour.size()-2);
int[] nv = contour.elementAt(contour.size()-1);
int dX = nv[0]-v[0];
int dY = nv[1]-v[1];
for (int j = 0; j < contour.size();) {
v = nv;
nv = contour.elementAt(j);
int ndX = nv[0]-v[0];
int ndY = nv[1]-v[1];
if (dX == ndX && dY == ndY) contour.removeElementAt(j>0? j-1 : contour.size()-1);
else j++;
dX = ndX;
dY = ndY;
}
contour.add(contour.elementAt(0));
}
private static final int[][] next = {
{ 0, -1 },
{ 1, 0 },
{ 0, 1 },
{ -1, 0 },
};
private static final int RUP = 1;
private static final int RDOWN = 2;
private static final byte[] remove = { RUP, 0, RDOWN, 0 };
/**
* trace bitmap, remove one contour from border list, note removed borders
* separately, return contour vertices
*/
private static final Vector<int[]> getContour(boolean[][][] border, int x, int y, int start, byte[][] removed) {
Vector<int[]> contour = new Vector<int[]>();
int nx, ny, ortho, dir = start;//, sx = x, sy = y;
// find start of current segment
nx = x - next[dir][0];
ny = y - next[dir][1];
while (nx >= 0 && ny >= 0 && ny < border.length && nx < border[ny].length && border[ny][nx][dir]) {
x = nx;
y = ny;
nx -= next[dir][0];
ny -= next[dir][1];
}
// check if we started on a sloped segment, adjust start
ortho = (dir-1)&3;
nx = x + next[ortho][0];
ny = y + next[ortho][1];
if (nx >= 0 && ny >= 0 && ny < border.length && nx < border[ny].length && border[ny][nx][(dir+1)&3]) {
nx -= next[dir][0];
ny -= next[dir][1];
if (nx >= 0 && ny >= 0 && ny < border.length && nx < border[ny].length && border[ny][nx][dir]) {
x += next[ortho][0];
y += next[ortho][1];
} else {
dir = ortho;
}
} else {
dir = ortho;
}
contour.add(new int[] { 2*x, 2*y });
// main loop
while (true) {
// assumption at start: border[dir] is false
int length = 0;
boolean sslope = false;
int[] eoff = { 0, 0 };
if (border[y][x][(dir+1)&3]) dir = (dir+1)&3;
else if (border[y][x][ortho]) dir = ortho;
else break;
ortho = (dir-1)&3;
// follow new segment as long as it lasts
while (border[y][x][dir]) {
border[y][x][dir] = false;
if (removed != null) removed[y][x] |= remove[dir];
x += next[dir][0];
y += next[dir][1];
length++;
}
// this is actually a sloped segment
if (length == 1 && border[y][x][ortho]) {
sslope = true;
dir = ortho;
ortho = (dir-1)&3;
contour.lastElement()[0] -= next[dir][0];
contour.lastElement()[1] -= next[dir][1];
// follow new segment as long as it lasts
length = 0;
while (border[y][x][dir]) {
border[y][x][dir] = false;
if (removed != null) removed[y][x] |= remove[dir];
x += next[dir][0];
y += next[dir][1];
length++;
}
}
// check if the end is sloped
if (border[y][x][ortho]) {
nx = x + next[ortho][0];
ny = y + next[ortho][1];
if (!border[ny][nx][ortho] && !border[ny][nx][(ortho-1)&3] && (border[ny][nx][dir] || start == dir)) {
// add two segments if this is a concave slope
if (sslope) contour.add(new int[] { 2*x-length*next[dir][0], 2*y-length*next[dir][1] });
border[y][x][ortho] = false;
if (removed != null) removed[y][x] |= remove[ortho];
x = nx;
y = ny;
eoff = next[dir];
dir = ortho;
}
}
contour.add(new int[] { 2*x+eoff[0], 2*y+eoff[1] });
}
return contour;
}
// find holes by scanning the inside of a removed contour, return first hole contour
private static final Vector<int[]> getInterior(boolean[][][] border, int x, int y, byte[][] removed) {
int inside = 0;
for (; y < removed.length; y++) {
for (; x < removed[y].length; x++) {
if (inside == 1 && border[y-1][x][DOWN]) return getContour(border, x, y-1, DOWN, null);
if (inside > 0) {
if (border[y][x][UP]) inside++;
else if (border[y-1][x][DOWN]) inside--;
}
if ((removed[y][x]&RUP) == RUP) inside = 1;
else if ((removed[y-1][x]&RDOWN) == RDOWN) inside = 0;
}
x = 0;
}
return null;
}
}