/** * Copyright 2010-2023 JogAmp Community. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation are those of the * authors and should not be interpreted as representing official policies, either expressed * or implied, of JogAmp Community. */ package com.jogamp.graph.ui.gl; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import com.jogamp.opengl.FPSCounter; import com.jogamp.opengl.GL; import com.jogamp.opengl.GL2ES2; import com.jogamp.opengl.GLAutoDrawable; import com.jogamp.opengl.GLCapabilitiesImmutable; import com.jogamp.opengl.GLEventListener; import com.jogamp.opengl.GLRunnable; import com.jogamp.opengl.fixedfunc.GLMatrixFunc; import com.jogamp.graph.curve.Region; import com.jogamp.graph.curve.opengl.RegionRenderer; import com.jogamp.graph.curve.opengl.RenderState; import com.jogamp.graph.geom.SVertex; import com.jogamp.graph.geom.Vertex; import com.jogamp.newt.event.GestureHandler; import com.jogamp.newt.event.InputEvent; import com.jogamp.newt.event.MouseEvent; import com.jogamp.newt.event.MouseListener; import com.jogamp.newt.event.PinchToZoomGesture; import com.jogamp.newt.event.GestureHandler.GestureEvent; import com.jogamp.newt.opengl.GLWindow; import com.jogamp.opengl.math.FloatUtil; import com.jogamp.opengl.math.Ray; import com.jogamp.opengl.math.geom.AABBox; import com.jogamp.opengl.util.PMVMatrix; /** * GraphUI Scene *

* GraphUI is GPU based and resolution independent. *

*

* GraphUI is intended to become an immediate- and retained-mode API. *

* @see Shape */ public class Scene implements GLEventListener { /** Default scene distance on z-axis to projection is -1/5f. */ public static final float DEFAULT_SCENE_DIST = -1/5f; /** Default projection z-near value is 0.1. */ public static final float DEFAULT_ZNEAR = 0.1f; /** Default projection z-far value is 7000. */ public static final float DEFAULT_ZFAR = 7000.0f; private static final boolean DEBUG = false; private final ArrayList shapes = new ArrayList(); private final float sceneDist, zNear, zFar; private final RegionRenderer renderer; private final int[] sampleCount = new int[1]; /** Describing the bounding box in shape's object model-coordinates of the near-plane parallel at its scene-distance. */ private final AABBox scenePlane = new AABBox(0f, 0f, 0f, 0f, 0f, 0f); private final int[] viewport = new int[] { 0, 0, 0, 0 }; private volatile Shape activeShape = null; private SBCMouseListener sbcMouseListener = null; private SBCGestureListener sbcGestureListener = null; private PinchToZoomGesture pinchToZoomGesture = null; private GLAutoDrawable cDrawable = null; private static RegionRenderer createRenderer() { final RenderState rs = RenderState.createRenderState(SVertex.factory()); return RegionRenderer.create(rs, RegionRenderer.defaultBlendEnable, RegionRenderer.defaultBlendDisable); } /** * Create a new scene with an internally created RegionRenderer * and using default values {@link #DEFAULT_SCENE_DIST}, {@link #DEFAULT_ZNEAR} and {@link #DEFAULT_ZFAR}. */ public Scene() { this(createRenderer(), DEFAULT_SCENE_DIST, DEFAULT_ZNEAR, DEFAULT_ZFAR); } /** * Create a new scene with given projection values and an internally created RegionRenderer. * @param sceneDist scene distance on z-axis to projection, consider using {@link #DEFAULT_SCENE_DIST}. * @param zNear projection z-near value, consider using {@link #DEFAULT_ZNEAR} * @param zFar projection z-far value, consider using {@link #DEFAULT_ZFAR} */ public Scene(final float sceneDist, final float zNear, final float zFar) { this(createRenderer(), sceneDist, zNear, zFar); } /** * Create a new scene taking ownership of the given RegionRenderer * and using default values {@link #DEFAULT_SCENE_DIST}, {@link #DEFAULT_ZNEAR} and {@link #DEFAULT_ZFAR}. */ public Scene(final RegionRenderer renderer) { this(renderer, DEFAULT_SCENE_DIST, DEFAULT_ZNEAR, DEFAULT_ZFAR); } /** * Create a new scene with given projection values and taking ownership of the given RegionRenderer. * @param renderer RegionRenderer to use and own * @param sceneDist scene distance on z-axis to projection, consider using {@link #DEFAULT_SCENE_DIST}. * @param zNear projection z-near value, consider using {@link #DEFAULT_ZNEAR} * @param zFar projection z-far value, consider using {@link #DEFAULT_ZFAR} */ public Scene(final RegionRenderer renderer, final float sceneDist, final float zNear, final float zFar) { if( null == renderer ) { throw new IllegalArgumentException("Null RegionRenderer"); } this.renderer = renderer; this.sceneDist = sceneDist; this.zFar = zFar; this.zNear = zNear; this.sampleCount[0] = 4; } /** Return z-axis distance of scene to projection, see {@link #DEFAULT_SCENE_DIST}. */ public float getProjSceneDist() { return sceneDist; } /** Return projection z-near value, see {@link #DEFAULT_ZNEAR}. */ public float getProjZNear() { return zNear; } /** Return projection z-far value, see {@link #DEFAULT_ZFAR}. */ public float getProjZFar() { return zFar; } /** Returns the associated RegionRenderer */ public RegionRenderer getRenderer() { return renderer; } /** Returns the associated RegionRenderer's RenderState, may be null. */ public RenderState getRenderState() { if( null != renderer ) { return renderer.getRenderState(); } return null; } public final Vertex.Factory getVertexFactory() { if( null != renderer ) { return renderer.getRenderState().getVertexFactory(); } return null; } public void attachInputListenerTo(final GLWindow window) { if(null == sbcMouseListener) { sbcMouseListener = new SBCMouseListener(); window.addMouseListener(sbcMouseListener); sbcGestureListener = new SBCGestureListener(); window.addGestureListener(sbcGestureListener); pinchToZoomGesture = new PinchToZoomGesture(window.getNativeSurface(), false); window.addGestureHandler(pinchToZoomGesture); } } public void detachInputListenerFrom(final GLWindow window) { if(null != sbcMouseListener) { window.removeMouseListener(sbcMouseListener); sbcMouseListener = null; window.removeGestureListener(sbcGestureListener); sbcGestureListener = null; window.removeGestureHandler(pinchToZoomGesture); pinchToZoomGesture = null; } } public ArrayList getShapes() { return shapes; } public void addShape(final Shape b) { shapes.add(b); } public void removeShape(final Shape b) { shapes.remove(b); } public final Shape getShapeByIdx(final int id) { if( 0 > id ) { return null; } return shapes.get(id); } public Shape getShapeByName(final int name) { for(final Shape b : shapes) { if(b.getName() == name ) { return b; } } return null; } public int getSampleCount() { return sampleCount[0]; } public int setSampleCount(final int v) { sampleCount[0] = Math.min(8, Math.max(v, 1)); // clip markAllShapesDirty(); return sampleCount[0]; } public void setAllShapesQuality(final int q) { for(int i=0; i shapeZAscComparator = new Comparator() { @Override public int compare(final Shape s1, final Shape s2) { final float s1Z = s1.getBounds().getMinZ()+s1.getPosition()[2]; final float s2Z = s2.getBounds().getMinZ()+s2.getPosition()[2]; if( FloatUtil.isEqual(s1Z, s2Z, FloatUtil.EPSILON) ) { return 0; } else if( s1Z < s2Z ){ return -1; } else { return 1; } } }; @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public void display(final GLAutoDrawable drawable) { final GL2ES2 gl = drawable.getGL().getGL2ES2(); gl.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); final PMVMatrix pmv = renderer.getMatrix(); pmv.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); final Object[] shapesS = shapes.toArray(); Arrays.sort(shapesS, (Comparator)shapeZAscComparator); renderer.enable(gl, true); //final int shapeCount = shapes.size(); final int shapeCount = shapesS.length; for(int i=0; i=0; i--) { final Shape uiShape = (Shape)shapesS[i]; if( uiShape.isEnabled() ) { pmv.glPushMatrix(); uiShape.setTransform(pmv); final boolean ok = pmv.gluUnProjectRay(glWinX, glWinY, winZ0, winZ1, viewport, 0, ray); pmv.glPopMatrix(); if( ok ) { final AABBox sbox = uiShape.getBounds(); if( sbox.intersectsRay(ray) ) { // System.err.printf("Pick.0: shape %d, [%d, %d, %f/%f] -> %s%n", i, glWinX, glWinY, winZ0, winZ1, ray); if( null == sbox.getRayIntersection(objPos, ray, FloatUtil.EPSILON, true, dpyTmp1V3, dpyTmp2V3, dpyTmp3V3) ) { throw new InternalError("Ray "+ray+", box "+sbox); } // System.err.printf("Pick.1: shape %d @ [%f, %f, %f], within %s%n", i, objPos[0], objPos[1], objPos[2], uiShape.getBounds()); return uiShape; } } } } return null; } private final float[] dpyTmp1V3 = new float[3]; private final float[] dpyTmp2V3 = new float[3]; private final float[] dpyTmp3V3 = new float[3]; /** * Calling {@link Shape#winToObjCoord(RegionRenderer, int, int, float[])}, retrieving its object position. * @param shape * @param glWinX in GL window coordinates, origin bottom-left * @param glWinY in GL window coordinates, origin bottom-left * @param objPos resulting object position * @param runnable action */ public void winToObjCoord(final Shape shape, final int glWinX, final int glWinY, final float[] objPos, final Runnable runnable) { if( null == cDrawable || null == shape ) { return; } cDrawable.invoke(false, new GLRunnable() { @Override public boolean run(final GLAutoDrawable drawable) { final boolean ok; { final PMVMatrix pmv = renderer.getMatrix(); pmv.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); pmv.glPushMatrix(); shape.setTransform(pmv); ok = shape.winToObjCoord(renderer, glWinX, glWinY, objPos); pmv.glPopMatrix(); } if( ok ) { runnable.run(); } return true; } } ); } /** * Disposes all {@link #addShape(Shape) added} {@link Shape}s. *

* Implementation also issues {@link RegionRenderer#destroy(GL2ES2)} if set * and {@link #detachInputListenerFrom(GLWindow)} in case the drawable is of type {@link GLWindow}. *

*

* {@inheritDoc} *

*/ @Override public void dispose(final GLAutoDrawable drawable) { System.err.println("SceneUIController: dispose"); if( drawable instanceof GLWindow ) { final GLWindow glw = (GLWindow) drawable; detachInputListenerFrom(glw); } final GL2ES2 gl = drawable.getGL().getGL2ES2(); for(int i=0; i * Modelview is translated to given {@link #getProjSceneDist()} * and and origin 0/0 becomes the lower-left corner. *

* @see #getScenePlane() */ @SuppressWarnings("unused") @Override public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) { viewport[0] = x; viewport[1] = y; viewport[2] = width; viewport[3] = height; final PMVMatrix pmv = renderer.getMatrix(); renderer.reshapePerspective(45.0f, width, height, zNear, zFar); pmv.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); pmv.glLoadIdentity(); pmv.glTranslatef(0f, 0f, sceneDist); { final float orthoDist = -sceneDist; final float[] obj00Coord = new float[3]; final float[] obj11Coord = new float[3]; final float[] winZ = new float[1]; winToObjCoord(pmv, viewport, zNear, zFar, 0f, 0f, orthoDist, winZ, obj00Coord); winToObjCoord(pmv, viewport, zNear, zFar, width, height, orthoDist, winZ, obj11Coord); pmv.glTranslatef(obj00Coord[0], obj00Coord[1], 0f); // lower-left corder origin 0/0 if( true ) { scenePlane.setSize( obj00Coord[0], // lx obj00Coord[1], // ly obj00Coord[2], // lz obj11Coord[0], // hx obj11Coord[1], // hy obj11Coord[2] );// hz } else { scenePlane.setSize( 0f, // lx 0f, // ly 0f, // lz obj11Coord[0] - obj00Coord[0], // hx obj11Coord[1] - obj00Coord[1], // hy obj11Coord[2] - obj00Coord[2]); // hz } if( true || DEBUG ) { System.err.printf("Reshape: zNear %f, zFar %f, sceneDist %f%n", zNear, zFar, sceneDist); System.err.printf("Reshape: Frustum: %s%n", pmv.glGetFrustum()); System.err.printf("Reshape: mapped.00: [%f, %f, %f], winZ %f -> [%f, %f, %f]%n", 0f, 0f, orthoDist, winZ[0], obj00Coord[0], obj00Coord[1], obj00Coord[2]); System.err.printf("Reshape: mapped.11: [%f, %f, %f], winZ %f -> [%f, %f, %f]%n", (float)width, (float)height, orthoDist, winZ[0], obj11Coord[0], obj11Coord[1], obj11Coord[2]); System.err.printf("Reshape: scenePlaneBox: %s%n", scenePlane); } } if( false ) { final float[] sceneScale = new float[3]; final float[] scenePlaneOrigin = new float[3]; scenePlaneOrigin[0] = scenePlane.getMinX() * sceneDist; scenePlaneOrigin[1] = scenePlane.getMinY() * sceneDist; scenePlaneOrigin[2] = scenePlane.getMinZ() * sceneDist; sceneScale[0] = ( scenePlane.getWidth() * sceneDist ) / width; sceneScale[1] = ( scenePlane.getHeight() * sceneDist ) / height; sceneScale[2] = 1f; System.err.printf("Scene Origin [%f, %f, %f]%n", scenePlaneOrigin[0], scenePlaneOrigin[1], scenePlaneOrigin[2]); System.err.printf("Scene Scale %f * [%f x %f] / [%d x %d] = [%f, %f, %f]%n", sceneDist, scenePlane.getWidth(), scenePlane.getHeight(), width, height, sceneScale[0], sceneScale[1], sceneScale[2]); } } /** Translate modelview to the {@link #getProjSceneDist()}, a convenience method. */ public void translate(final PMVMatrix pmv) { // pmv.glTranslatef(0f, 0f, sceneDist); pmv.glTranslatef(scenePlane.getMinX(), scenePlane.getMinY(), 0f); } /** * Describing the scene's object model-dimensions of the near-plane parallel at its scene-distance {@link #getProjSceneDist()} * having the origin 0/0 on the lower-left corner. *

* The value is evaluated at {@link #reshape(GLAutoDrawable, int, int, int, int)} before translating to the lower-left origin 0/0, * i.e. its minimum values are negative of half dimension. *

*

* {@link AABBox#getWidth()} and {@link AABBox#getHeight()} define scene's dimension covered by surface size. *

*/ public AABBox getScenePlane() { return scenePlane; } public final Shape getActiveShape() { return activeShape; } public void release() { setActiveShape(null); } private void setActiveShape(final Shape shape) { activeShape = shape; } private final class SBCGestureListener implements GestureHandler.GestureListener { @Override public void gestureDetected(final GestureEvent gh) { if( null != activeShape ) { // gesture .. delegate to active shape! final InputEvent orig = gh.getTrigger(); if( orig instanceof MouseEvent ) { final MouseEvent e = (MouseEvent) orig; // flip to GL window coordinates final int glWinX = e.getX(); final int glWinY = viewport[3] - e.getY() - 1; final float[] objPos = new float[3]; final Shape shape = activeShape; winToObjCoord(shape, glWinX, glWinY, objPos, new Runnable() { @Override public void run() { shape.dispatchGestureEvent(renderer, gh, glWinX, glWinY, objPos); } } ); } } } } /** * Dispatch mouse event, either directly sending to activeShape or picking one * @param e original Newt {@link MouseEvent} * @param glWinX in GL window coordinates, origin bottom-left * @param glWinY in GL window coordinates, origin bottom-left */ final void dispatchMouseEvent(final MouseEvent e, final int glWinX, final int glWinY) { if( null == activeShape ) { dispatchMouseEventPickShape(e, glWinX, glWinY, true); } else { dispatchMouseEventForShape(activeShape, e, glWinX, glWinY); } } /** * Pick the shape using the event coordinates * @param e original Newt {@link MouseEvent} * @param glWinX in GL window coordinates, origin bottom-left * @param glWinY in GL window coordinates, origin bottom-left * @param setActive */ final void dispatchMouseEventPickShape(final MouseEvent e, final int glWinX, final int glWinY, final boolean setActive) { final float[] objPos = new float[3]; final Shape[] shape = { null }; pickShape(glWinX, glWinY, objPos, shape, new Runnable() { @Override public void run() { if( setActive ) { setActiveShape(shape[0]); } shape[0].dispatchMouseEvent(e, glWinX, glWinY, objPos); } } ); } /** * Dispatch event to shape * @param shape target active shape of event * @param e original Newt {@link MouseEvent} * @param glWinX in GL window coordinates, origin bottom-left * @param glWinY in GL window coordinates, origin bottom-left */ final void dispatchMouseEventForShape(final Shape shape, final MouseEvent e, final int glWinX, final int glWinY) { final float[] objPos = new float[3]; winToObjCoord(shape, glWinX, glWinY, objPos, new Runnable() { @Override public void run() { shape.dispatchMouseEvent(e, glWinX, glWinY, objPos); } } ); } private class SBCMouseListener implements MouseListener { int lx=-1, ly=-1, lId=-1; void clear() { lx = -1; ly = -1; lId = -1; } @Override public void mousePressed(final MouseEvent e) { if( -1 == lId || e.getPointerId(0) == lId ) { lx = e.getX(); ly = e.getY(); lId = e.getPointerId(0); } // flip to GL window coordinates, origin bottom-left final int glWinX = e.getX(); final int glWinY = viewport[3] - e.getY() - 1; dispatchMouseEvent(e, glWinX, glWinY); } @Override public void mouseReleased(final MouseEvent e) { // flip to GL window coordinates, origin bottom-left final int glWinX = e.getX(); final int glWinY = viewport[3] - e.getY() - 1; dispatchMouseEvent(e, glWinX, glWinY); if( 1 == e.getPointerCount() ) { // Release active shape: last pointer has been lifted! release(); clear(); } } @Override public void mouseClicked(final MouseEvent e) { // flip to GL window coordinates final int glWinX = e.getX(); final int glWinY = viewport[3] - e.getY() - 1; // activeId should have been released by mouseRelease() already! dispatchMouseEventPickShape(e, glWinX, glWinY, false); // Release active shape: last pointer has been lifted! release(); clear(); } @Override public void mouseDragged(final MouseEvent e) { // drag activeShape, if no gesture-activity, only on 1st pointer if( null != activeShape && !pinchToZoomGesture.isWithinGesture() && e.getPointerId(0) == lId ) { lx = e.getX(); ly = e.getY(); // dragged .. delegate to active shape! // flip to GL window coordinates, origin bottom-left final int glWinX = lx; final int glWinY = viewport[3] - ly - 1; dispatchMouseEventForShape(activeShape, e, glWinX, glWinY); } } @Override public void mouseWheelMoved(final MouseEvent e) { // flip to GL window coordinates final int glWinX = lx; final int glWinY = viewport[3] - ly - 1; dispatchMouseEventPickShape(e, glWinX, glWinY, true); } @Override public void mouseMoved(final MouseEvent e) { if( -1 == lId || e.getPointerId(0) == lId ) { lx = e.getX(); ly = e.getY(); lId = e.getPointerId(0); } } @Override public void mouseEntered(final MouseEvent e) { } @Override public void mouseExited(final MouseEvent e) { release(); clear(); } } /** * Return a formatted status string containing avg fps and avg frame duration. * @param glad GLAutoDrawable instance for FPSCounter, its chosen GLCapabilities and its GL's swap-interval * @param renderModes render modes for {@link Region#getRenderModeString(int)} * @param quality the Graph-Curve quality setting * @param dpi the monitor's DPI (vertical preferred) * @return formatted status string */ public String getStatusText(final GLAutoDrawable glad, final int renderModes, final int quality, final float dpi) { final FPSCounter fpsCounter = glad.getAnimator(); final float lfps, tfps, td; if( null != fpsCounter ) { lfps = fpsCounter.getLastFPS(); tfps = fpsCounter.getTotalFPS(); td = (float)fpsCounter.getLastFPSPeriod() / (float)fpsCounter.getUpdateFPSFrames(); } else { lfps = 0f; tfps = 0f; td = 0f; } final String modeS = Region.getRenderModeString(renderModes); final GLCapabilitiesImmutable caps = glad.getChosenGLCapabilities(); return String.format("%03.1f/%03.1f fps, %.1f ms/f, v-sync %d, dpi %.1f, %s-samples %d, q %d, msaa %d, blend %b, alpha %d", lfps, tfps, td, glad.getGL().getSwapInterval(), dpi, modeS, getSampleCount(), quality, caps.getNumSamples(), getRenderState().isHintMaskSet(RenderState.BITHINT_BLENDING_ENABLED), caps.getAlphaBits()); } /** * Return a formatted status string containing avg fps and avg frame duration. * @param fpsCounter the counter, must not be null * @return formatted status string */ public static String getStatusText(final FPSCounter fpsCounter) { final float lfps = fpsCounter.getLastFPS(); final float tfps = fpsCounter.getTotalFPS(); final float td = (float)fpsCounter.getLastFPSPeriod() / (float)fpsCounter.getUpdateFPSFrames(); return String.format("%03.1f/%03.1f fps, %.1f ms/f", lfps, tfps, td); } }