diff options
author | Sven Gothel <[email protected]> | 2023-03-31 11:06:44 +0200 |
---|---|---|
committer | Sven Gothel <[email protected]> | 2023-03-31 11:06:44 +0200 |
commit | 4f32f3aba62a73cafecec8af461cff4d0d475882 (patch) | |
tree | 58f6e7f0800a1ca8b793966520d6008cc6f2080b /src/graphui/classes/com/jogamp/graph/ui/Scene.java | |
parent | 759a381963476e2e64b7afba0d6ef2bafeb35417 (diff) |
GraphUI: Relocate com.jogamp.graph.ui.gl.* -> com.jogamp.graph.ui.*, resolve GL/VK abstraction at a later time differently
Actual GPU rendering toolkit dependency can be abstracted differently, i.e. GPU <- { GL, VK } etc.
Diffstat (limited to 'src/graphui/classes/com/jogamp/graph/ui/Scene.java')
-rw-r--r-- | src/graphui/classes/com/jogamp/graph/ui/Scene.java | 1073 |
1 files changed, 1073 insertions, 0 deletions
diff --git a/src/graphui/classes/com/jogamp/graph/ui/Scene.java b/src/graphui/classes/com/jogamp/graph/ui/Scene.java new file mode 100644 index 000000000..cf0f96b28 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/Scene.java @@ -0,0 +1,1073 @@ +/** + * 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; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Locale; + +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.GLException; +import com.jogamp.opengl.GLRunnable; +import com.jogamp.opengl.fixedfunc.GLMatrixFunc; +import com.jogamp.common.nio.Buffers; +import com.jogamp.graph.curve.Region; +import com.jogamp.graph.curve.opengl.RegionRenderer; +import com.jogamp.graph.curve.opengl.RenderState; +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.GLPixelStorageModes; +import com.jogamp.opengl.util.GLReadBufferUtil; +import com.jogamp.opengl.util.PMVMatrix; + +/** + * GraphUI Scene + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + * <p> + * GraphUI is intended to become an immediate- and retained-mode API. + * </p> + * <p> + * To utilize a Scene instance directly as a {@link GLEventListener}, + * user needs to {@link #setClearParams(float[], int)}. + * + * Otherwise user may just call provided {@link GLEventListener} from within their own workflow + * - {@link GLEventListener#init(GLAutoDrawable)} + * - {@link GLEventListener#reshape(GLAutoDrawable, int, int, int, int)} + * - {@link GLEventListener#display(GLAutoDrawable)} + * - {@link GLEventListener#dispose(GLAutoDrawable)} + * </p> + * <p> + * {@link #setPMVMatrixSetup(PMVMatrixSetup)} maybe used to provide a custom {@link PMVMatrix} setup. + * </p> + * @see Shape + */ +public final 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 angle in degrees value is 45.0. */ + public static final float DEFAULT_ANGLE = 45.0f; + /** 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; + + @SuppressWarnings("unused") + private static final boolean DEBUG = false; + + private final ArrayList<Shape> shapes = new ArrayList<Shape>(); + private float dbgbox_thickness = 0f; + private boolean doFrustumCulling = false; + + private float[] clearColor = null; + private int clearMask; + + 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, post {@link #translate(PMVMatrix)} */ + private final AABBox planeBox = new AABBox(0f, 0f, 0f, 0f, 0f, 0f); + + private volatile Shape activeShape = null; + + private SBCMouseListener sbcMouseListener = null; + private SBCGestureListener sbcGestureListener = null; + private PinchToZoomGesture pinchToZoomGesture = null; + + final GLReadBufferUtil screenshot; + + private GLAutoDrawable cDrawable = null; + + private static RegionRenderer createRenderer() { + return RegionRenderer.create(RegionRenderer.defaultBlendEnable, RegionRenderer.defaultBlendDisable); + } + + /** + * Create a new scene with an internally created RegionRenderer + * and using default values {@link #DEFAULT_SCENE_DIST}, {@link #DEFAULT_ANGLE}, {@link #DEFAULT_ZNEAR} and {@link #DEFAULT_ZFAR}. + */ + public Scene() { + this(createRenderer()); + } + + /** + * Create a new scene taking ownership of the given RegionRenderer + * and using default values {@link #DEFAULT_SCENE_DIST}, {@link #DEFAULT_ANGLE}, {@link #DEFAULT_ZNEAR} and {@link #DEFAULT_ZFAR}. + */ + public Scene(final RegionRenderer renderer) { + if( null == renderer ) { + throw new IllegalArgumentException("Null RegionRenderer"); + } + this.renderer = renderer; + this.sampleCount[0] = 4; + this.screenshot = new GLReadBufferUtil(false, false); + } + + /** Returns the associated RegionRenderer */ + public RegionRenderer getRenderer() { return renderer; } + + /** Returns the associated RegionRenderer's RenderState. */ + public RenderState getRenderState() { return renderer.getRenderState(); } + + /** + * Sets the clear parameter for {@link GL#glClearColor(float, float, float, float) glClearColor(..)} and {@link GL#glClear(int) glClear(..)} + * to be issued at {@link #display(GLAutoDrawable)}. + * + * Without setting these parameter, user has to issue + * {@link GL#glClearColor(float, float, float, float) glClearColor(..)} and {@link GL#glClear(int) glClear(..)} + * before calling {@link #display(GLAutoDrawable)}. + * + * @param clearColor {@link GL#glClearColor(float, float, float, float) glClearColor(..)} arguments + * @param clearMask {@link GL#glClear(int) glClear(..)} mask, default is {@link GL#GL_COLOR_BUFFER_BIT} | {@link GL#GL_DEPTH_BUFFER_BIT} + */ + public final void setClearParams(final float[] clearColor, final int clearMask) { this.clearColor = clearColor; this.clearMask = clearMask; } + + /** Returns the {@link GL#glClearColor(float, float, float, float) glClearColor(..)} arguments, see {@link #setClearParams(float[], int)}. */ + public final float[] getClearColor() { return clearColor; } + + /** Returns the {@link GL#glClear(int) glClear(..)} mask, see {@link #setClearParams(float[], int)}. */ + public final int getClearMask() { return clearMask; } + + /** Enable or disable {@link PMVMatrix#glGetFrustum()} culling per {@link Shape}. Default is disabled. */ + public final void setFrustumCullingEnabled(final boolean v) { doFrustumCulling = v; } + + /** Return whether {@link #setFrustumCullingEnabled(boolean) frustum culling} is enabled. */ + public final boolean isFrustumCullingEnabled() { return doFrustumCulling; } + + 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<Shape> getShapes() { + return shapes; + } + public void addShape(final Shape s) { + s.setDebugBox(dbgbox_thickness); + shapes.add(s); + } + /** Removes given shape, keeps it alive. */ + public void removeShape(final Shape s) { + s.setDebugBox(0f); + shapes.remove(s); + } + /** Removes all given shapes and destroys them. */ + public void removeShape(final GL2ES2 gl, final Shape s) { + s.setDebugBox(0f); + shapes.remove(s); + s.destroy(gl, renderer); + } + public void addShapes(final Collection<? extends Shape> shapes) { + for(final Shape s : shapes) { + addShape(s); + } + } + /** Removes all given shapes, keeps them alive. */ + public void removeShapes(final Collection<? extends Shape> shapes) { + for(final Shape s : shapes) { + removeShape(s); + } + } + /** Removes all given shapes and destroys them. */ + public void removeShapes(final GL2ES2 gl, final Collection<? extends Shape> shapes) { + for(final Shape s : shapes) { + removeShape(gl, s); + } + } + public 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, 0)); // clip + markAllShapesDirty(); + return sampleCount[0]; + } + + public void setAllShapesQuality(final int q) { + for(int i=0; i<shapes.size(); i++) { + final Shape shape = shapes.get(i); + if( shape instanceof GraphShape ) { + ((GraphShape)shape).setQuality(q); + } + } + } + public void setAllShapesSharpness(final float sharpness) { + for(int i=0; i<shapes.size(); i++) { + final Shape shape = shapes.get(i); + if( shape instanceof GraphShape ) { + ((GraphShape)shape).setSharpness(sharpness); + } + } + } + public void markAllShapesDirty() { + for(int i=0; i<shapes.size(); i++) { + shapes.get(i).markShapeDirty(); + } + } + + /** + * Sets the {@link #getBounds()} fractional thickness of the debug box ranging [0..1] for all shapes, zero for no debug box (default). + * @param v fractional thickness of {@link #getBounds()} ranging [0..1], zero for no debug box + */ + public final void setDebugBox(final float v) { + dbgbox_thickness = v; + for(int i=0; i<shapes.size(); i++) { + shapes.get(i).setDebugBox(v); + } + } + + @Override + public void init(final GLAutoDrawable drawable) { + cDrawable = drawable; + renderer.init(drawable.getGL().getGL2ES2()); + } + + /** + * Reshape scene using {@link #setupMatrix(PMVMatrix, int, int, int, int)} using {@link PMVMatrixSetup}. + * <p> + * {@inheritDoc} + * </p> + * @see PMVMatrixSetup + * @see #setPMVMatrixSetup(PMVMatrixSetup) + * @see #setupMatrix(PMVMatrix, int, int, int, int) + * @see #getBounds() + * @see #getBoundsCenter() + */ + @Override + public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) { + renderer.reshapeNotify(x, y, width, height); + + setupMatrix(renderer.getMatrix(), x, y, width, height); + pmvMatrixSetup.setPlaneBox(planeBox, renderer.getMatrix(), x, y, width, height); + } + + private static Comparator<Shape> shapeZAscComparator = new Comparator<Shape>() { + @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 Object[] shapesS = shapes.toArray(); + Arrays.sort(shapesS, (Comparator)shapeZAscComparator); + + display(drawable, shapesS, false); + } + + private static final int[] sampleCountGLSelect = { -1 }; + + private void display(final GLAutoDrawable drawable, final Object[] shapes, final boolean glSelect) { + final GL2ES2 gl = drawable.getGL().getGL2ES2(); + + final int[] sampleCount0; + if( glSelect ) { + gl.glClearColor(0f, 0f, 0f, 1f); + sampleCount0 = sampleCountGLSelect; + gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); + } else { + if( null != clearColor ) { + gl.glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); + gl.glClear(clearMask); + } + sampleCount0 = sampleCount; + } + + final PMVMatrix pmv = renderer.getMatrix(); + pmv.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); + + if( glSelect ) { + renderer.enable(gl, true, RegionRenderer.defaultBlendDisable, RegionRenderer.defaultBlendDisable); + } else { + renderer.enable(gl, true); + } + + //final int shapeCount = shapes.size(); + final int shapeCount = shapes.length; + for(int i=0; i<shapeCount; i++) { + // final UIShape uiShape = shapes.get(i); + final Shape shape = (Shape)shapes[i]; + // System.err.println("Id "+i+": "+uiShape); + if( shape.isEnabled() ) { + pmv.glPushMatrix(); + shape.setTransform(pmv); + + if( !doFrustumCulling || !pmv.glGetFrustum().isAABBoxOutside( shape.getBounds() ) ) { + if( glSelect ) { + final float color = ( i + 1f ) / ( shapeCount + 2f ); + // FIXME + // System.err.printf("drawGL: color %f, index %d of [0..%d[%n", color, i, shapeCount); + renderer.getRenderState().setColorStatic(color, color, color, 1f); + shape.drawToSelect(gl, renderer, sampleCount0); + } else { + shape.draw(gl, renderer, sampleCount0); + } + } + pmv.glPopMatrix(); + } + } + if( glSelect ) { + renderer.enable(gl, false, RegionRenderer.defaultBlendDisable, RegionRenderer.defaultBlendDisable); + } else { + renderer.enable(gl, false); + } + synchronized ( syncDisplayedOnce ) { + displayedOnce = true; + syncDisplayedOnce.notifyAll(); + } + } + + private volatile boolean displayedOnce = false; + private final Object syncDisplayedOnce = new Object(); + + /** Blocks until first {@link #display(GLAutoDrawable)} has completed after construction or {@link #dispose(GLAutoDrawable). */ + public void waitUntilDisplayed() { + synchronized( syncDisplayedOnce ) { + while( !displayedOnce ) { + try { + syncDisplayedOnce.wait(); + } catch (final InterruptedException e) { } + } + } + } + + /** + * Disposes all {@link #addShape(Shape) added} {@link Shape}s. + * <p> + * Implementation also issues {@link RegionRenderer#destroy(GL2ES2)} if set + * and {@link #detachInputListenerFrom(GLWindow)} in case the drawable is of type {@link GLWindow}. + * </p> + * <p> + * {@inheritDoc} + * </p> + */ + @Override + public void dispose(final GLAutoDrawable drawable) { + synchronized ( syncDisplayedOnce ) { + displayedOnce = false; + syncDisplayedOnce.notifyAll(); + } + if( drawable instanceof GLWindow ) { + final GLWindow glw = (GLWindow) drawable; + detachInputListenerFrom(glw); + } + final GL2ES2 gl = drawable.getGL().getGL2ES2(); + for(int i=0; i<shapes.size(); i++) { + shapes.get(i).destroy(gl, renderer); + } + shapes.clear(); + cDrawable = null; + renderer.destroy(gl); + screenshot.dispose(gl); + } + + /** + * Attempt to pick a {@link Shape} using the window coordinates and contained {@ling Shape}'s {@link AABBox} {@link Shape#getBounds() bounds} + * using a ray-intersection algorithm. + * <p> + * If {@link Shape} was found the given action is performed. + * </p> + * <p> + * Method performs on current thread and returns after probing every {@link Shape}. + * </p> + * @param glWinX window X coordinate, bottom-left origin + * @param glWinY window Y coordinate, bottom-left origin + * @param pmv a new {@link PMVMatrix} which will {@link Scene.PMVMatrixSetup#set(PMVMatrix, int, int, int, int) be setup}, + * {@link Shape#setTransform(PMVMatrix) shape-transformed} and can be reused by the caller and runnable. + * @param objPos storage for found object position in model-space of found {@link Shape} + * @param shape storage for found {@link Shape} or null + * @param runnable the action to perform if {@link Shape} was found + */ + public Shape pickShape(final int glWinX, final int glWinY, final PMVMatrix pmv, final float[] objPos, final Shape[] shape, final Runnable runnable) { + shape[0] = pickShapeImpl(glWinX, glWinY, pmv, objPos); + if( null != shape[0] ) { + runnable.run(); + } + return shape[0]; + } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Shape pickShapeImpl(final int glWinX, final int glWinY, final PMVMatrix pmv, final float[] objPos) { + final float winZ0 = 0f; + final float winZ1 = 0.3f; + /** + final FloatBuffer winZRB = Buffers.newDirectFloatBuffer(1); + gl.glReadPixels( x, y, 1, 1, GL2ES2.GL_DEPTH_COMPONENT, GL.GL_FLOAT, winZRB); + winZ1 = winZRB.get(0); // dir + */ + setupMatrix(pmv); + + final Ray ray = new Ray(); + + final Object[] shapesS = shapes.toArray(); + Arrays.sort(shapesS, (Comparator)shapeZAscComparator); + + for(int i=shapesS.length-1; 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, getViewport(), 0, ray); + 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; + } + } + pmv.glPopMatrix(); // we leave the stack open if picked above, allowing the modelview shape transform to be reused + } + } + return null; + } + private final float[] dpyTmp1V3 = new float[3]; + private final float[] dpyTmp2V3 = new float[3]; + private final float[] dpyTmp3V3 = new float[3]; + + /** + * Attempt to pick a {@link Shape} using the OpenGL false color rendering. + * <p> + * If {@link Shape} was found the given action is performed on the rendering thread. + * </p> + * <p> + * Method is non blocking and performs on rendering-thread, it returns immediately. + * </p> + * @param glWinX window X coordinate, bottom-left origin + * @param glWinY window Y coordinate, bottom-left origin + * @param objPos storage for found object position in model-space of found {@link Shape} + * @param shape storage for found {@link Shape} or null + * @param runnable the action to perform if {@link Shape} was found + */ + public void pickShapeGL(final int glWinX, final int glWinY, final float[] objPos, final Shape[] shape, final Runnable runnable) { + if( null == cDrawable ) { + return; + } + cDrawable.invoke(false, new GLRunnable() { + @Override + public boolean run(final GLAutoDrawable drawable) { + final Shape s = pickShapeGLImpl(drawable, glWinX, glWinY); + shape[0] = s; + if( null != s ) { + final PMVMatrix pmv = renderer.getMatrix(); + pmv.glPushMatrix(); + s.setTransform(pmv); + final boolean ok = null != shape[0].winToShapeCoord(getMatrix(), getViewport(), glWinX, glWinY, objPos); + pmv.glPopMatrix(); + if( ok ) { + runnable.run(); + } + } + return false; // needs to re-render to wash away our false-color glSelect + } } ); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Shape pickShapeGLImpl(final GLAutoDrawable drawable, final int glWinX, final int glWinY) { + final Object[] shapesS = shapes.toArray(); + Arrays.sort(shapesS, (Comparator)shapeZAscComparator); + + final GLPixelStorageModes psm = new GLPixelStorageModes(); + final ByteBuffer pixel = Buffers.newDirectByteBuffer(4); + + final GL2ES2 gl = drawable.getGL().getGL2ES2(); + + display(drawable, shapesS, true); + + psm.setPackAlignment(gl, 4); + // psm.setUnpackAlignment(gl, 4); + try { + // gl.glReadPixels(glWinX, getHeight() - glWinY, 1, 1, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, pixel); + gl.glReadPixels(glWinX, glWinY, 1, 1, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, pixel); + } catch(final GLException gle) { + gle.printStackTrace(); + return null; + } + psm.restore(gl); + + // final float color = ( i + 1f ) / ( shapeCount + 2f ); + final int shapeCount = shapes.size(); + final int qp = pixel.get(0) & 0xFF; + final float color = qp / 255.0f; + final int index = Math.round( ( color * ( shapeCount + 2f) ) - 1f ); + + // FIXME drawGL: color 0.333333, index 0 of [0..1[ + System.err.printf("pickGL: glWin %d / %d, byte %d, color %f, index %d of [0..%d[%n", + glWinX, glWinY, qp, color, index, shapeCount); + + if( 0 <= index && index < shapeCount ) { + return (Shape)shapesS[index]; + } else { + return null; + } + } + + /** + * Calling {@link Shape#winToObjCoord(Scene, int, int, float[])}, retrieving its Shape object position. + * @param shape + * @param glWinX in GL window coordinates, origin bottom-left + * @param glWinY in GL window coordinates, origin bottom-left + * @param pmv a new {@link PMVMatrix} which will {@link Scene.PMVMatrixSetup#set(PMVMatrix, int, int, int, int) be setup}, + * {@link Shape#setTransform(PMVMatrix) shape-transformed} and can be reused by the caller and runnable. + * @param objPos resulting object position + * @param runnable action + */ + public void winToShapeCoord(final Shape shape, final int glWinX, final int glWinY, final PMVMatrix pmv, final float[] objPos, final Runnable runnable) { + if( null != shape && null != shape.winToShapeCoord(pmvMatrixSetup, renderer.getViewport(), glWinX, glWinY, pmv, objPos) ) { + runnable.run(); + } + } + + /** + * Interface providing {@link #set(PMVMatrix, int, int, int, int) a method} to + * setup {@link PMVMatrix}'s {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW}. + * <p> + * At the end of operations, the {@link GLMatrixFunc#GL_MODELVIEW} matrix has to be selected. + * </p> + * <p> + * Implementation is being called by {@link Scene#setupMatrix(PMVMatrix, int, int, int, int)} + * and hence {@link Scene#reshape(GLAutoDrawable, int, int, int, int)}. + * </p> + * <p> + * Custom implementations can be set via {@link Scene#setPMVMatrixSetup(PMVMatrixSetup)}. + * </p> + * <p> + * The default implementation is described below: + * <ul> + * <li>{@link GLMatrixFunc#GL_PROJECTION} Matrix + * <ul> + * <li>Identity</li> + * <li>Perspective {@link Scene#DEFAULT_ANGLE} with {@link Scene#DEFAULT_ZNEAR} and {@link Scene#DEFAULT_ZFAR}</li> + * <li>Translated to given {@link Scene#DEFAULT_SCENE_DIST}</li> + * <li>Scale (back) to have normalized {@link Scene#getBounds() plane dimensions}, 1 for the greater of width and height.</li> + * </ul></li> + * <li>{@link GLMatrixFunc#GL_MODELVIEW} Matrix + * <ul> + * <li>identity</li> + * </ul></li> + * </ul> + * </p> + */ + public static interface PMVMatrixSetup { + /** + * Setup {@link PMVMatrix}'s {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW}. + * <p> + * See {@link PMVMatrixSetup} for details. + * </p> + * <p> + * At the end of operations, the {@link GLMatrixFunc#GL_MODELVIEW} matrix is selected. + * </p> + * @param pmv the {@link PMVMatrix} to setup + * @param x lower left corner of the viewport rectangle + * @param y lower left corner of the viewport rectangle + * @param width width of the viewport rectangle + * @param height height of the viewport rectangle + */ + void set(PMVMatrix pmv, final int x, final int y, final int width, final int height); + + /** + * Optional method to set the {@link Scene#getBounds()} {@link AABBox}, maybe a {@code nop} if not desired. + * <p> + * Will be called by {@link Scene#reshape(GLAutoDrawable, int, int, int, int)} after {@link #set(PMVMatrix, int, int, int, int)}. + * </p> + * @param planeBox the {@link AABBox} to define + * @param pmv the {@link PMVMatrix}, already setup via {@link #set(PMVMatrix, int, int, int, int)}. + * @param x lower left corner of the viewport rectangle + * @param y lower left corner of the viewport rectangle + * @param width width of the viewport rectangle + * @param height height of the viewport rectangle + */ + void setPlaneBox(final AABBox planeBox, final PMVMatrix pmv, int x, int y, final int width, final int height); + } + + /** Return the default or {@link #setPMVMatrixSetup(PMVMatrixSetup)} {@link PMVMatrixSetup}. */ + public final PMVMatrixSetup getPMVMatrixSetup() { return pmvMatrixSetup; } + + /** Set a custom {@link PMVMatrixSetup}. */ + public final void setPMVMatrixSetup(final PMVMatrixSetup setup) { pmvMatrixSetup = setup; } + + /** Return the default {@link PMVMatrixSetup}. */ + public static PMVMatrixSetup getDefaultPMVMatrixSetup() { return defaultPMVMatrixSetup; } + + /** + * Setup {@link PMVMatrix} {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW} + * by calling {@link #getPMVMatrixSetup()}'s {@link PMVMatrixSetup#set(PMVMatrix, int, int, int, int)}. + * @param pmv the {@link PMVMatrix} to setup + * @param x lower left corner of the viewport rectangle + * @param y lower left corner of the viewport rectangle + * @param width width of the viewport rectangle + * @param height height of the viewport rectangle + */ + public void setupMatrix(final PMVMatrix pmv, final int x, final int y, final int width, final int height) { + pmvMatrixSetup.set(pmv, x, y, width, height); + } + + /** + * Setup {@link PMVMatrix} {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW} + * using implicit {@link #getViewport()} surface dimension by calling {@link #getPMVMatrixSetup()}'s {@link PMVMatrixSetup#set(PMVMatrix, int, int, int, int)}. + * @param pmv the {@link PMVMatrix} to setup + */ + public void setupMatrix(final PMVMatrix pmv) { + final int[] viewport = renderer.getViewport(); + setupMatrix(pmv, viewport[0], viewport[1], viewport[2], viewport[3]); + } + + /** Copies the current int[4] viewport in given target and returns it for chaining. It is set after initial {@link #reshape(GLAutoDrawable, int, int, int, int)}. */ + public final int[/*4*/] getViewport(final int[/*4*/] target) { return renderer.getViewport(target); } + + /** Borrows the current int[4] viewport w/o copying. It is set after initial {@link #reshape(GLAutoDrawable, int, int, int, int)}. */ + public int[/*4*/] getViewport() { return renderer.getViewport(); } + + /** Returns the {@link #getViewport()}'s width, set after initial {@link #reshape(GLAutoDrawable, int, int, int, int)}. */ + public int getWidth() { return renderer.getWidth(); } + /** Returns the {@link #getViewport()}'s height, set after initial {@link #reshape(GLAutoDrawable, int, int, int, int)}. */ + public int getHeight() { return renderer.getHeight(); } + + /** Borrow the current {@link PMVMatrix}. */ + public PMVMatrix getMatrix() { return renderer.getMatrix(); } + + /** + * Describing the scene's object model-dimensions of the plane at scene-distance covering the visible viewport rectangle. + * <p> + * The value is evaluated at {@link #reshape(GLAutoDrawable, int, int, int, int)} via {@link } + * </p> + * <p> + * {@link AABBox#getWidth()} and {@link AABBox#getHeight()} define scene's dimension covered by surface size. + * </p> + * <p> + * {@link AABBox} is setup via {@link #getPMVMatrixSetup()}'s {@link PMVMatrixSetup#setPlaneBox(AABBox, PMVMatrix, int, int, int, int)}. + * </p> + * <p> + * The default {@link PMVMatrixSetup} implementation scales to normalized plane dimensions, 1 for the greater of width and height. + * </p> + */ + public AABBox getBounds() { return planeBox; } + + /** + * + * @param pmv + * @param viewport + * @param zNear + * @param zFar + * @param winX + * @param winY + * @param objOrthoZ + * @param objPos float[3] storage for object coord result + * @param winZ + */ + public static void winToPlaneCoord(final PMVMatrix pmv, final int[] viewport, + final float zNear, final float zFar, + final float winX, final float winY, final float objOrthoZ, + final float[] objPos) { + final float winZ = FloatUtil.getOrthoWinZ(objOrthoZ, zNear, zFar); + pmv.gluUnProject(winX, winY, winZ, viewport, 0, objPos, 0); + } + + /** + * Map given window surface-size to object coordinates relative to this scene using + * the give projection parameters. + * @param viewport viewport rectangle + * @param zNear custom {@link #DEFAULT_ZNEAR} + * @param zFar custom {@link #DEFAULT_ZFAR} + * @param objOrthoDist custom {@link #DEFAULT_SCENE_DIST} + * @param objSceneSize float[2] storage for object surface size result + */ + public void surfaceToPlaneSize(final int[] viewport, final float zNear, final float zFar, final float objOrthoDist, final float[/*2*/] objSceneSize) { + final PMVMatrix pmv = new PMVMatrix(); + setupMatrix(pmv, viewport[0], viewport[1], viewport[2], viewport[3]); + { + final float[] obj00Coord = new float[3]; + final float[] obj11Coord = new float[3]; + + winToPlaneCoord(pmv, viewport, DEFAULT_ZNEAR, DEFAULT_ZFAR, viewport[0], viewport[1], objOrthoDist, obj00Coord); + winToPlaneCoord(pmv, viewport, DEFAULT_ZNEAR, DEFAULT_ZFAR, viewport[2], viewport[3], objOrthoDist, obj11Coord); + objSceneSize[0] = obj11Coord[0] - obj00Coord[0]; + objSceneSize[1] = obj11Coord[1] - obj00Coord[1]; + } + } + + /** + * Map given window surface-size to object coordinates relative to this scene using + * the default {@link PMVMatrixSetup}, i.e. {@link #DEFAULT_ZNEAR}, {@link #DEFAULT_ZFAR} and {@link #DEFAULT_SCENE_DIST} + * @param viewport viewport rectangle + * @param objSceneSize float[2] storage for object surface size result + */ + public void surfaceToPlaneSize(final int[] viewport, final float[/*2*/] objSceneSize) { + surfaceToPlaneSize(viewport, DEFAULT_ZNEAR, DEFAULT_ZFAR, -DEFAULT_SCENE_DIST, objSceneSize); + } + + public final Shape getActiveShape() { + return activeShape; + } + + public void releaseActiveShape() { + activeShape = 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 = getHeight() - e.getY() - 1; + final PMVMatrix pmv = new PMVMatrix(); + final float[] objPos = new float[3]; + final Shape shape = activeShape; + winToShapeCoord(shape, glWinX, glWinY, pmv, objPos, () -> { + shape.dispatchGestureEvent(gh, glWinX, glWinY, pmv, renderer.getViewport(), 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); + } 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 + */ + final void dispatchMouseEventPickShape(final MouseEvent e, final int glWinX, final int glWinY) { + final PMVMatrix pmv = new PMVMatrix(); + final float[] objPos = new float[3]; + final Shape[] shape = { null }; + if( null == pickShape(glWinX, glWinY, pmv, objPos, shape, () -> { + setActiveShape(shape[0]); + shape[0].dispatchMouseEvent(e, glWinX, glWinY, objPos); + } ) ) + { + releaseActiveShape(); + } + } + /** + * 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 PMVMatrix pmv = new PMVMatrix(); + final float[] objPos = new float[3]; + winToShapeCoord(shape, glWinX, glWinY, pmv, objPos, () -> { 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 = getHeight() - 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 = getHeight() - e.getY() - 1; + dispatchMouseEvent(e, glWinX, glWinY); + if( 1 == e.getPointerCount() ) { + // Release active shape: last pointer has been lifted! + releaseActiveShape(); + clear(); + } + } + + @Override + public void mouseClicked(final MouseEvent e) { + // flip to GL window coordinates + final int glWinX = e.getX(); + final int glWinY = getHeight() - e.getY() - 1; + // activeId should have been released by mouseRelease() already! + dispatchMouseEventPickShape(e, glWinX, glWinY); + // Release active shape: last pointer has been lifted! + releaseActiveShape(); + 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 = getHeight() - 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 = getHeight() - ly - 1; + dispatchMouseEvent(e, glWinX, glWinY); + } + + @Override + public void mouseMoved(final MouseEvent e) { + if( -1 == lId || e.getPointerId(0) == lId ) { + lx = e.getX(); + ly = e.getY(); + lId = e.getPointerId(0); + } + final int glWinX = lx; + final int glWinY = getHeight() - ly - 1; + // dispatchMouseEvent(e, glWinX, glWinY); + dispatchMouseEventPickShape(e, glWinX, glWinY); + } + @Override + public void mouseEntered(final MouseEvent e) { } + @Override + public void mouseExited(final MouseEvent e) { + releaseActiveShape(); + 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 or -1 to be ignored + * @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(); + final String sampleCountStr1, sampleCountStr2, qualityStr, blendStr; + if( Region.isVBAA(renderModes) || Region.isMSAA(renderModes) ) { + sampleCountStr1 = "-samples "+getSampleCount(); + } else { + sampleCountStr1 = ""; + } + if( caps.getNumSamples() > 0 ) { + sampleCountStr2 = ", smsaa "+caps.getNumSamples(); + } else { + sampleCountStr2 = ""; + } + if( 0 <= quality ) { + qualityStr = ", q "+quality; + } else { + qualityStr = ""; + } + if( getRenderState().isHintMaskSet(RenderState.BITHINT_BLENDING_ENABLED) ) { + blendStr = ", blend"; + } else { + blendStr = ""; + } + return String.format("%03.1f/%03.1f fps, %.1f ms/f, vsync %d, dpi %.1f, %s%s%s%s%s, a %d", + lfps, tfps, td, glad.getGL().getSwapInterval(), dpi, modeS, sampleCountStr1, sampleCountStr2, + qualityStr, blendStr, 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); + } + + /** + * Write current read drawable (screen) to a PNG file. + * @see #getScreenshotCount() + */ + public void screenshot(final GL gl, final int renderModes, final String prefix) { + final RegionRenderer renderer = getRenderer(); + final String modeS = Region.getRenderModeString(renderModes); + final String filename = String.format((Locale)null, "%s-shot%03d-%03dx%03d-S_%s_%02d.png", + prefix, shotCount++, renderer.getWidth(), renderer.getHeight(), + modeS, getSampleCount()); + gl.glFinish(); // just make sure rendering finished .. + if(screenshot.readPixels(gl, false)) { + screenshot.write(new File(filename)); + System.err.println("Wrote: "+filename); + } + } + private int shotCount = 0; + + /** Return the number of {@link #screenshot(GL, int, String)}s being taken. */ + public int getScreenshotCount() { return shotCount; } + + private static final PMVMatrixSetup defaultPMVMatrixSetup = new PMVMatrixSetup() { + @Override + public void set(final PMVMatrix pmv, final int x, final int y, final int width, final int height) { + final float ratio = (float)width/(float)height; + pmv.glMatrixMode(GLMatrixFunc.GL_PROJECTION); + pmv.glLoadIdentity(); + pmv.gluPerspective(DEFAULT_ANGLE, ratio, DEFAULT_ZNEAR, DEFAULT_ZFAR); + pmv.glTranslatef(0f, 0f, DEFAULT_SCENE_DIST); + + pmv.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); + pmv.glLoadIdentity(); + + // Scale (back) to have normalized plane dimensions, 1 for the greater of width and height. + final AABBox planeBox0 = new AABBox(); + setPlaneBox(planeBox0, pmv, x, y, width, height); + final float sx = planeBox0.getWidth(); + final float sy = planeBox0.getHeight(); + final float sxy = sx > sy ? sx : sy; + pmv.glMatrixMode(GLMatrixFunc.GL_PROJECTION); + pmv.glScalef(sxy, sxy, 1f); + pmv.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); + } + + @Override + public void setPlaneBox(final AABBox planeBox, final PMVMatrix pmv, final int x, final int y, final int width, final int height) { + final float orthoDist = -DEFAULT_SCENE_DIST; + final float[] obj00Coord = new float[3]; + final float[] obj11Coord = new float[3]; + final int[] viewport = { x, y, width, height }; + + winToPlaneCoord(pmv, viewport, DEFAULT_ZNEAR, DEFAULT_ZFAR, x, y, orthoDist, obj00Coord); + winToPlaneCoord(pmv, viewport, DEFAULT_ZNEAR, DEFAULT_ZFAR, width, height, orthoDist, obj11Coord); + + planeBox.setSize( obj00Coord[0], // lx + obj00Coord[1], // ly + obj00Coord[2], // lz + obj11Coord[0], // hx + obj11Coord[1], // hy + obj11Coord[2] );// hz } + } + }; + private PMVMatrixSetup pmvMatrixSetup = defaultPMVMatrixSetup; +} |