package de.lmu.ifi.dbs.elki.visualization.parallel3d;
/*
This file is part of ELKI:
Environment for Developing KDD-Applications Supported by Index-Structures
Copyright (C) 2015
Ludwig-Maximilians-Universität München
Lehr- und Forschungseinheit für Datenbanksysteme
ELKI Development Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
import java.awt.Color;
import java.awt.geom.Rectangle2D;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.Arrays;
import javax.media.opengl.GL;
import javax.media.opengl.GL2;
import javax.media.opengl.GLAutoDrawable;
import de.lmu.ifi.dbs.elki.data.NumberVector;
import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
import de.lmu.ifi.dbs.elki.logging.Logging;
import de.lmu.ifi.dbs.elki.math.MathUtil;
import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
import de.lmu.ifi.dbs.elki.utilities.documentation.Reference;
import de.lmu.ifi.dbs.elki.utilities.io.ByteArrayUtil;
import de.lmu.ifi.dbs.elki.utilities.pairs.DoubleIntPair;
import de.lmu.ifi.dbs.elki.utilities.pairs.IntIntPair;
import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
import de.lmu.ifi.dbs.elki.visualization.parallel3d.OpenGL3DParallelCoordinates.Instance.Shared;
import de.lmu.ifi.dbs.elki.visualization.parallel3d.layout.Layout;
import de.lmu.ifi.dbs.elki.visualization.parallel3d.layout.Layout.Node;
import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy;
import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
/**
* Renderer for 3D parallel plots.
*
* The tricky part here is the vertex buffer layout. We are drawing lines, so we
* need two vertices for each macro edge (edge between axes in the plot). We
* furthermore need the following properties: we need to draw edges sorted by
* depth to allow alpha and smoothing to work, and we need to be able to have
* different colors for clusters. An efficient batch therefore will consist of
* one edge-color combination. The input data comes in color-object ordering, so
* we need to seek through the edges when writing the buffer.
*
* In total, we have 2 * obj.size * edges.size vertices.
*
* Where obj.size = sum(col.sizes)
*
* Reference:
*
* Elke Achtert, Hans-Peter Kriegel, Erich Schubert, Arthur Zimek:
* Interactive Data Mining with 3D-Parallel-Coordinate-Trees.
* Proceedings of the 2013 ACM International Conference on Management of Data
* (SIGMOD), New York City, NY, 2013.
*
*
* TODO: generalize to non-numeric features and scales.
*
* @author Erich Schubert
* @since 0.6.0
*
* @param Object type
*/
@Reference(authors = "Elke Achtert, Hans-Peter Kriegel, Erich Schubert, Arthur Zimek", title = "Interactive Data Mining with 3D-Parallel-Coordinate-Trees", booktitle = "Proc. of the 2013 ACM International Conference on Management of Data (SIGMOD)", url = "http://dx.doi.org/10.1145/2463676.2463696")
public class Parallel3DRenderer {
/**
* Logging class.
*/
private static final Logging LOG = Logging.getLogger(Parallel3DRenderer.class);
/**
* Shared data.
*/
Shared shared;
/**
* Prerendered textures.
*/
private int[] textures;
/**
* Number of completely rendered textures.
*/
private int completedTextures = 0;
/**
* Depth indexes of axes.
*/
private int[] dindex;
/**
* Color table.
*/
float[] colors;
/**
* Axes sorting array.
*/
DoubleIntPair[] axes;
/**
* Vertex buffer.
*/
int[] vbi = new int[] { -1 };
/**
* Framebuffer for render-to-texture
*/
int[] frameBufferID = new int[] { -1 };
/**
* Constructor.
*
* @param shared Shared data.
*/
protected Parallel3DRenderer(Shared shared) {
super();
this.shared = shared;
this.dindex = new int[shared.dim];
axes = new DoubleIntPair[shared.dim];
for (int i = 0; i < shared.dim; i++) {
axes[i] = new DoubleIntPair(0.0, 0);
}
}
protected int prepare(GL2 gl) {
if (completedTextures < 0) {
if (textures != null) {
gl.glDeleteTextures(textures.length, textures, 0);
textures = null;
}
completedTextures = 0;
}
if (completedTextures >= shared.layout.edges.size()) {
return 0;
}
if (!LOG.isDebugging()) {
renderTexture(gl, completedTextures);
} else {
long start = System.nanoTime();
renderTexture(gl, completedTextures);
long end = System.nanoTime();
LOG.debug("Time to render texture: " + (end - start) / 1e6 + " ms.");
}
return (completedTextures < shared.layout.edges.size()) ? 1 : 2;
}
protected void drawParallelPlot(GLAutoDrawable drawable, GL2 gl) {
// Sort axes by sq. distance from camera, front-to-back:
sortAxes();
// Sort edges by the maximum (foreground) index.
IntIntPair[] edgesort = sortEdges(dindex);
if (textures != null) {
gl.glShadeModel(GL2.GL_FLAT);
// Render spider web:
gl.glLineWidth(shared.settings.linewidth); // outside glBegin!
gl.glBegin(GL.GL_LINES);
gl.glColor4f(0f, 0f, 0f, 1f);
for (Layout.Edge edge : shared.layout.edges) {
Node n1 = shared.layout.getNode(edge.dim1), n2 = shared.layout.getNode(edge.dim2);
gl.glVertex3d(n1.getX(), n1.getY(), 0f);
gl.glVertex3d(n2.getX(), n2.getY(), 0f);
}
gl.glEnd();
// Draw axes and 3DPC:
for (int i = 0; i < shared.dim; i++) {
final int d = axes[i].second;
final Node node1 = shared.layout.getNode(d);
// Draw edge textures
for (IntIntPair pair : edgesort) {
// Not yet available?
if (pair.second >= completedTextures) {
continue;
}
// Other axis must have a smaller index.
if (pair.first >= i) {
continue;
}
Layout.Edge edge = shared.layout.edges.get(pair.second);
// Must involve the current axis.
if (edge.dim1 != d && edge.dim2 != d) {
continue;
}
int od = axes[pair.first].second;
gl.glEnable(GL.GL_TEXTURE_2D);
gl.glColor4f(1f, 1f, 1f, 1f);
final Node node2 = shared.layout.getNode(od);
gl.glBindTexture(GL.GL_TEXTURE_2D, textures[pair.second]);
gl.glBegin(GL2.GL_QUADS);
gl.glTexCoord2d((edge.dim1 == d) ? 0f : 1f, 0f);
gl.glVertex3d(node1.getX(), node1.getY(), 0f);
gl.glTexCoord2d((edge.dim1 == d) ? 0f : 1f, 1f);
gl.glVertex3d(node1.getX(), node1.getY(), 1f);
gl.glTexCoord2d((edge.dim1 != d) ? 0f : 1f, 1f);
gl.glVertex3d(node2.getX(), node2.getY(), 1f);
gl.glTexCoord2d((edge.dim1 != d) ? 0f : 1f, 0f);
gl.glVertex3d(node2.getX(), node2.getY(), 0f);
gl.glEnd();
gl.glDisable(GL.GL_TEXTURE_2D);
}
// Draw axis
gl.glLineWidth(shared.settings.linewidth); // outside glBegin!
gl.glBegin(GL.GL_LINES);
gl.glColor4f(0f, 0f, 0f, 1f);
gl.glVertex3d(node1.getX(), node1.getY(), 0f);
gl.glVertex3d(node1.getX(), node1.getY(), 1f);
gl.glEnd();
// Draw ticks.
LinearScale scale = shared.proj.getAxisScale(d);
gl.glPointSize(shared.settings.linewidth * 2f);
gl.glBegin(GL.GL_POINTS);
for (double tick = scale.getMin(); tick <= scale.getMax() + scale.getRes() / 10; tick += scale.getRes()) {
gl.glVertex3d(node1.getX(), node1.getY(), scale.getScaled(tick));
}
gl.glEnd();
}
}
// Render labels
renderLabels(gl, edgesort);
}
void renderTexture(GL2 gl, int edge) {
assert (edge == completedTextures);
// Setup color table:
prepareColors(shared.stylepol);
// Setup buffer IDs:
if (vbi[0] < 0) {
gl.glGenBuffers(1, vbi, 0);
// Buffer for coordinates.
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vbi[0]);
gl.glBufferData(GL.GL_ARRAY_BUFFER, shared.rel.size() // Number of lines *
* 2 // 2 Points *
* 5 // 2 coordinates + 3 color
* ByteArrayUtil.SIZE_FLOAT, null, GL2.GL_DYNAMIC_DRAW);
} else {
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vbi[0]);
}
// Generate textures:
if (textures == null) {
textures = new int[shared.layout.edges.size()];
gl.glGenTextures(textures.length, textures, 0);
}
// Get a framebuffer:
if (frameBufferID[0] < 0) {
gl.glGenFramebuffers(1, frameBufferID, 0);
}
gl.glPushAttrib(GL2.GL_TEXTURE_BIT | GL2.GL_VIEWPORT_BIT);
gl.glPushMatrix();
gl.glBindFramebuffer(GL2.GL_FRAMEBUFFER, frameBufferID[0]);
{
Layout.Edge e = shared.layout.edges.get(edge);
gl.glBindTexture(GL.GL_TEXTURE_2D, textures[edge]);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL2.GL_TEXTURE_WRAP_S, GL2.GL_CLAMP_TO_EDGE);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL2.GL_TEXTURE_WRAP_T, GL2.GL_CLAMP_TO_EDGE);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL2.GL_TEXTURE_MIN_FILTER, GL2.GL_LINEAR);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL2.GL_TEXTURE_MAG_FILTER, GL2.GL_LINEAR);
gl.glTexEnvi(GL2.GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_MODE, GL2.GL_MODULATE);
// Reserve texture image data:
gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL2.GL_RGBA16, //
shared.settings.texwidth, shared.settings.texheight, 0, // Size
GL2.GL_RGBA, GL2.GL_FLOAT, null);
gl.glViewport(0, 0, shared.settings.texwidth, shared.settings.texheight);
// Attach 2D texture to this FBO
gl.glFramebufferTexture2D(GL2.GL_FRAMEBUFFER, GL2.GL_COLOR_ATTACHMENT0, //
GL.GL_TEXTURE_2D, textures[edge], 0);
if (gl.glCheckFramebufferStatus(GL2.GL_FRAMEBUFFER) != GL2.GL_FRAMEBUFFER_COMPLETE) {
LOG.warning("glCheckFramebufferStatus: " + gl.glCheckFramebufferStatus(GL2.GL_FRAMEBUFFER));
}
gl.glDisable(GL2.GL_LIGHTING);
gl.glDisable(GL.GL_CULL_FACE);
gl.glDisable(GL.GL_DEPTH_TEST);
gl.glMatrixMode(GL2.GL_PROJECTION);
gl.glLoadIdentity();
gl.glOrtho(0f, 1f, 0f, StyleLibrary.SCALE, -1, 1);
gl.glMatrixMode(GL2.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glClearColor(1f, 1f, 1f, .0f);
gl.glClear(GL2.GL_COLOR_BUFFER_BIT);
gl.glShadeModel(GL2.GL_SMOOTH);
gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
gl.glEnable(GL.GL_BLEND);
gl.glEnable(GL.GL_LINE_SMOOTH);
gl.glLineWidth(shared.settings.linewidth);
if (shared.stylepol instanceof ClassStylingPolicy) {
ClassStylingPolicy csp = (ClassStylingPolicy) shared.stylepol;
final int mincolor = csp.getMinStyle();
ByteBuffer vbytebuffer = gl.glMapBuffer(GL.GL_ARRAY_BUFFER, GL2.GL_WRITE_ONLY);
FloatBuffer vertices = vbytebuffer.order(ByteOrder.nativeOrder()).asFloatBuffer();
int p = 0;
for (DBIDIter it = shared.rel.iterDBIDs(); it.valid(); it.advance(), p += 2) {
final O vec = shared.rel.get(it);
final int c = (csp.getStyleForDBID(it) - mincolor) * 3;
final float v1 = (float) shared.proj.fastProjectDataToRenderSpace(vec.doubleValue(e.dim1), e.dim1);
final float v2 = (float) shared.proj.fastProjectDataToRenderSpace(vec.doubleValue(e.dim2), e.dim2);
vertices.put(0.f);
vertices.put(v1);
vertices.put(colors[c]);
vertices.put(colors[c + 1]);
vertices.put(colors[c + 2]);
vertices.put(1.f);
vertices.put(v2);
vertices.put(colors[c]);
vertices.put(colors[c + 1]);
vertices.put(colors[c + 2]);
}
vertices.flip();
gl.glUnmapBuffer(GL.GL_ARRAY_BUFFER);
gl.glColor4f(1f, 1f, 1f, 1f);
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vbi[0]);
gl.glVertexPointer(2, GL.GL_FLOAT, 5 * ByteArrayUtil.SIZE_FLOAT, 0);
gl.glEnableClientState(GL2.GL_VERTEX_ARRAY);
gl.glColorPointer(3, GL.GL_FLOAT, 5 * ByteArrayUtil.SIZE_FLOAT, 2 * ByteArrayUtil.SIZE_FLOAT);
gl.glEnableClientState(GL2.GL_COLOR_ARRAY);
gl.glDrawArrays(GL.GL_LINES, 0, p);
gl.glDisableClientState(GL2.GL_COLOR_ARRAY);
gl.glDisableClientState(GL2.GL_VERTEX_ARRAY);
} else {
ByteBuffer vbytebuffer = gl.glMapBuffer(GL.GL_ARRAY_BUFFER, GL2.GL_WRITE_ONLY);
FloatBuffer vertices = vbytebuffer.order(ByteOrder.nativeOrder()).asFloatBuffer();
int p = 0;
for (DBIDIter it = shared.rel.iterDBIDs(); it.valid(); it.advance(), p += 2) {
final O vec = shared.rel.get(it);
final float v1 = (float) shared.proj.fastProjectDataToRenderSpace(vec.doubleValue(e.dim1), e.dim1);
final float v2 = (float) shared.proj.fastProjectDataToRenderSpace(vec.doubleValue(e.dim2), e.dim2);
vertices.put(0.f);
vertices.put(v1);
vertices.put(1.f);
vertices.put(v2);
}
vertices.flip();
gl.glUnmapBuffer(GL.GL_ARRAY_BUFFER);
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vbi[0]);
gl.glEnableClientState(GL2.GL_VERTEX_ARRAY);
gl.glVertexPointer(2, GL.GL_FLOAT, 0, 0);
gl.glColor3f(colors[0], colors[1], colors[2]);
gl.glDrawArrays(GL.GL_LINES, 0, p);
gl.glDisableClientState(GL2.GL_VERTEX_ARRAY);
}
if (shared.settings.mipmaps > 0) {
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL2.GL_TEXTURE_BASE_LEVEL, 0);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL2.GL_TEXTURE_MAX_LEVEL, shared.settings.mipmaps);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL2.GL_TEXTURE_MAG_FILTER, GL2.GL_LINEAR);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL2.GL_TEXTURE_MIN_FILTER, GL2.GL_LINEAR_MIPMAP_LINEAR);
gl.glHint(GL.GL_GENERATE_MIPMAP_HINT, GL.GL_NICEST);
gl.glGenerateMipmap(GL.GL_TEXTURE_2D);
}
gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
if (!gl.glIsTexture(textures[0])) {
LOG.warning("Generating texture failed!");
}
}
// Switch back to the default framebuffer.
gl.glBindFramebuffer(GL2.GL_FRAMEBUFFER, 0);
gl.glPopMatrix();
gl.glPopAttrib();
++completedTextures;
if (completedTextures == shared.layout.edges.size()) {
// Free vertex buffer
gl.glDeleteBuffers(vbi.length, vbi, 0);
vbi[0] = -1;
// Free framebuffer
gl.glDeleteFramebuffers(1, frameBufferID, 0);
frameBufferID[0] = -1;
}
}
private void prepareColors(final StylingPolicy sp) {
if (colors == null) {
final ColorLibrary cols = shared.stylelib.getColorSet(StyleLibrary.PLOT);
if (sp instanceof ClassStylingPolicy) {
ClassStylingPolicy csp = (ClassStylingPolicy) sp;
final int maxStyle = csp.getMaxStyle();
colors = new float[maxStyle * 3];
for (int c = 0, s = csp.getMinStyle(); s < maxStyle; c += 3, s++) {
Color col = SVGUtil.stringToColor(cols.getColor(s));
colors[c + 0] = col.getRed() / 255.f;
colors[c + 1] = col.getGreen() / 255.f;
colors[c + 2] = col.getBlue() / 255.f;
}
} else {
// Render in black.
colors = new float[] { 0f, 0f, 0f };
}
}
}
protected void forgetTextures(GL gl) {
if (gl == null) {
completedTextures = -1;
} else {
if (textures != null) {
gl.glDeleteTextures(textures.length, textures, 0);
textures = null;
}
completedTextures = 0;
}
}
/**
* Depth-sort the axes.
*
* @param axes Sorted list of (depth, axis)
*/
private void sortAxes() {
for (int d = 0; d < shared.dim; d++) {
double dist = shared.camera.squaredDistanceFromCamera(shared.layout.getNode(d).getX(), shared.layout.getNode(d).getY());
axes[d].first = -dist;
axes[d].second = d;
}
Arrays.sort(axes);
for (int i = 0; i < shared.dim; i++) {
dindex[axes[i].second] = i;
}
}
/**
* Sort the edges for rendering.
*
* FIXME: THIS STILL HAS ERRORS SOMETIME!
*
* @param dindex depth index of axes.
* @return Sorted array of (minaxis, edgeid)
*/
private IntIntPair[] sortEdges(int[] dindex) {
IntIntPair[] edgesort = new IntIntPair[shared.layout.edges.size()];
int e = 0;
for (Layout.Edge edge : shared.layout.edges) {
int i1 = dindex[edge.dim1], i2 = dindex[edge.dim2];
edgesort[e] = new IntIntPair(Math.min(i1, i2), e);
e++;
}
Arrays.sort(edgesort);
return edgesort;
}
private void renderLabels(GL2 gl, IntIntPair[] edgesort) {
shared.textrenderer.begin3DRendering();
// UNDO the camera rotation. This will mess up text orientation!
gl.glRotatef((float) MathUtil.rad2deg(shared.camera.getRotationZ()), 0.f, 0.f, 1.f);
// Rotate to have the text face the camera direction, which looks +Y
// While the text will be visible from +Z and +Y is baseline.
gl.glRotatef(90.f, 1.f, 0.f, 0.f);
// HalfPI: 180 degree extra rotation, for text orientation.
double cos = Math.cos(shared.camera.getRotationZ()), sin = Math.sin(shared.camera.getRotationZ());
shared.textrenderer.setColor(0.0f, 0.0f, 0.0f, 1.0f);
float defaultscale = .01f / (float) Math.sqrt(shared.dim);
final float targetwidth = .2f; // TODO: div depth?
final float minratio = 8.f; // Assume all text is at least this width
for (int i = 0; i < shared.dim; i++) {
if (shared.labels[i] != null) {
Rectangle2D b = shared.textrenderer.getBounds(shared.labels[i]);
float scale = defaultscale;
if (Math.max(b.getWidth(), b.getHeight() * minratio) * scale > targetwidth) {
scale = targetwidth / (float) Math.max(b.getWidth(), b.getHeight() * minratio);
}
float w = (float) b.getWidth() * scale;
// Rotate manually, in x-z plane
float x = (float) (cos * shared.layout.getNode(i).getX() + sin * shared.layout.getNode(i).getY());
float y = (float) (-sin * shared.layout.getNode(i).getX() + cos * shared.layout.getNode(i).getY());
shared.textrenderer.draw3D(shared.labels[i], (x - w * .5f), 1.01f, -y, scale);
}
}
// Show depth indexes on debug:
if (OpenGL3DParallelCoordinates.Instance.DEBUG) {
shared.textrenderer.setColor(1f, 0f, 0f, 1f);
for (IntIntPair pair : edgesort) {
Layout.Edge edge = shared.layout.edges.get(pair.second);
final Node node1 = shared.layout.getNode(edge.dim1);
final Node node2 = shared.layout.getNode(edge.dim2);
final double mx = 0.5 * (node1.getX() + node2.getX());
final double my = 0.5 * (node1.getY() + node2.getY());
// Rotate manually, in x-z plane
float x = (float) (cos * mx + sin * my);
float y = (float) (-sin * mx + cos * my);
shared.textrenderer.draw3D(Integer.toString(pair.first), (x - defaultscale * .5f), 1.01f, -y, .5f * defaultscale);
}
}
shared.textrenderer.end3DRendering();
}
}