/* * JaamSim Discrete Event Simulation * Copyright (C) 2012 Ausenco Engineering Canada Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jaamsim.render; //import com.jaamsim.math.*; import java.awt.EventQueue; import java.awt.Font; import java.awt.Frame; import java.awt.Image; import java.awt.Window.Type; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.URL; import java.nio.IntBuffer; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; import com.jaamsim.DisplayModels.DisplayModel; import com.jaamsim.MeshFiles.MeshData; import com.jaamsim.font.OverlayString; import com.jaamsim.font.TessFont; import com.jaamsim.input.ColourInput; import com.jaamsim.math.AABB; import com.jaamsim.math.Color4d; import com.jaamsim.math.Ray; import com.jaamsim.math.Vec3d; import com.jaamsim.math.Vec4d; import com.jaamsim.render.util.ExceptionLogger; import com.jaamsim.ui.LogBox; import com.jogamp.common.util.VersionNumber; import com.jogamp.nativewindow.NativeWindowFactory; import com.jogamp.newt.event.WindowEvent; import com.jogamp.newt.event.WindowListener; import com.jogamp.newt.event.WindowUpdateEvent; import com.jogamp.newt.opengl.GLWindow; import com.jogamp.opengl.DebugGL4bc; import com.jogamp.opengl.GL; import com.jogamp.opengl.GL2GL3; import com.jogamp.opengl.GL3; import com.jogamp.opengl.GL4bc; import com.jogamp.opengl.GLAnimatorControl; import com.jogamp.opengl.GLAutoDrawable; import com.jogamp.opengl.GLCapabilities; import com.jogamp.opengl.GLContext; import com.jogamp.opengl.GLDrawableFactory; import com.jogamp.opengl.GLEventListener; import com.jogamp.opengl.GLException; import com.jogamp.opengl.GLProfile; /** * The central renderer for JaamSim Renderer, Contains references to all context * specific data (like shader caches) * * @author Matt.Chudleigh * */ public class Renderer implements GLAnimatorControl { public enum ShaderHandle { FONT, HULL, OVERLAY_FONT, OVERLAY_FLAT, DEBUG, SKYBOX } private static final AtomicInteger nextAssetID = new AtomicInteger(0); /** * Get a system wide unique ID * @return */ public static int getAssetID() { return nextAssetID.incrementAndGet(); } private static boolean USE_DEBUG_GL = true; public static int DIFF_TEX_FLAG = 1; public static int NUM_MESH_SHADERS = 2; // Should be 2^(max_flag) private EnumMap<ShaderHandle, Shader> shaders; private final Shader[] meshShaders = new Shader[NUM_MESH_SHADERS]; private GLContext sharedContext = null; Map<Integer, Integer> sharedVaoMap = new HashMap<>(); int sharedContextID = getAssetID(); GLAutoDrawable dummyDrawable; private GLCapabilities caps = null; private boolean gl3Supported; private final TexCache texCache = new TexCache(this); // An initalization time flag specifying if the 'safest' graphical techniques should be used private boolean safeGraphics; private final Thread renderThread; private final Object rendererLock = new Object(); private final Map<MeshProtoKey, MeshProto> protoCache; private final Map<TessFontKey, TessFont> fontCache; private final HashMap<Integer, RenderWindow> openWindows; private final HashMap<Integer, Camera> cameras; private final Queue<RenderMessage> renderMessages = new ArrayDeque<>(); private final AtomicBoolean displayNeeded = new AtomicBoolean(true); private final AtomicBoolean initialized = new AtomicBoolean(false); private final AtomicBoolean shutdown = new AtomicBoolean(false); private final AtomicBoolean fatalError = new AtomicBoolean(false); private String errorString; // This is the string that caused the fatal error private StackTraceElement[] fatalStackTrace; // the stack trace from the fatal error private final ExceptionLogger exceptionLogger; private final TessFontKey defaultFontKey = new TessFontKey(Font.SANS_SERIF, Font.PLAIN); private final TessFontKey defaultBoldFontKey = new TessFontKey(Font.SANS_SERIF, Font.BOLD); private final Object sceneLock = new Object(); private ArrayList<RenderProxy> proxyScene = new ArrayList<>(); private boolean allowDelayedTextures; private long sceneTimeNS; private long loopTimeNS; private final Object settingsLock = new Object(); private boolean showDebugInfo = false; private long usedVRAM = 0; // A flag to track JOGL's GLAnimatorControl pause feature private boolean isPaused = false; // This may not be the best way to cache this private GLContext drawContext = null; private Skybox skybox; private MeshData badData; private MeshProto badProto; // A cache of the current scene, needed by the individual windows to render private ArrayList<Renderable> currentScene = new ArrayList<>(); private ArrayList<OverlayRenderable> currentOverlay = new ArrayList<>(); public Renderer(boolean safeGraphics) throws RenderException { this.safeGraphics = safeGraphics; protoCache = new HashMap<>(); fontCache = new HashMap<>(); exceptionLogger = new ExceptionLogger(1); // Print the call stack on the first exception of any kind openWindows = new HashMap<>(); cameras = new HashMap<>(); renderThread = new Thread(new Runnable() { @Override public void run() { mainRenderLoop(); } }, "RenderThread"); renderThread.start(); } private void mainRenderLoop() { //long startNanos = System.nanoTime(); try { // GLProfile.initSingleton(); GLProfile glp = GLProfile.get(GLProfile.GL2GL3); caps = new GLCapabilities(glp); caps.setSampleBuffers(true); caps.setNumSamples(4); caps.setDepthBits(24); final boolean createNewDevice = true; dummyDrawable = GLDrawableFactory.getFactory(glp).createDummyAutoDrawable(null, createNewDevice, caps, null); dummyDrawable.display(); // triggers GLContext object creation and native realization. sharedContext = dummyDrawable.getContext(); assert (sharedContext != null); try { GL3 gl3 = sharedContext.getGL().getGL3(); gl3Supported = gl3 != null; } catch (GLException ex) { gl3Supported = false; } // long endNanos = System.nanoTime(); // long ms = (endNanos - startNanos) /1000000L; // LogBox.formatRenderLog("Creating shared context at:" + ms + "ms"); checkForIntelDriver(); initSharedContext(); // Notify the main thread we're done initialized.set(true); } catch (Exception e) { fatalError.set(true); errorString = e.getLocalizedMessage(); fatalStackTrace = e.getStackTrace(); LogBox.renderLog("Renderer encountered a fatal error:"); LogBox.renderLogException(e); } finally { if (sharedContext != null && sharedContext.isCurrent()) sharedContext.release(); } // endNanos = System.nanoTime(); // ms = (endNanos - startNanos) /1000000L; // LogBox.formatRenderLog("Started renderer loop after:" + ms + "ms"); long lastLoopEnd = System.nanoTime(); // Add a custom shutdown hook to make sure we're finished closing before JOGL tries to shutdown NativeWindowFactory.addCustomShutdownHook(true, new Runnable() { @Override public void run() { // Block JOGL shutting down until we're dead shutdown(); while (renderThread.isAlive()) { synchronized (this) { try { queueRedraw(); // Just in case the render thread got stalled somewhere wait(50); } catch (InterruptedException ex) {} } } } }); while (!shutdown.get()) { try { // If a fatal error was encountered, clean up the renderer if (fatalError.get()) { // We should clean up everything we can, then die try { for (Entry<Integer, RenderWindow> entry : openWindows.entrySet()){ entry.getValue().getGLWindowRef().destroy(); entry.getValue().getAWTFrameRef().dispose(); } } catch(Exception e) {} // Ignore any exceptions, this is just a best effort cleanup try { dummyDrawable.destroy(); sharedContext.destroy(); dummyDrawable = null; sharedContext = null; openWindows.clear(); currentScene = null; currentOverlay = null; caps = null; fontCache.clear(); protoCache.clear(); shaders.clear(); } catch (Exception e) { } break; // Exiting the loop will end the thread } displayNeeded.set(false); updateRenderableScene(); // Run all render messages boolean moreMessages = false; do { // Only lock the queue while reading messages, release it while // processing them RenderMessage message = null; synchronized (renderMessages) { if (!renderMessages.isEmpty()) { message = renderMessages.remove(); moreMessages = !renderMessages.isEmpty(); } } if (message != null) { try { handleMessage(message); } catch (Throwable t) { // Log this error but continue processing logException(t); } } } while (moreMessages); // Defensive copy the window list (in case a window is closed while we render) HashMap<Integer, RenderWindow> winds; synchronized (openWindows) { winds = new HashMap<>(openWindows); } if (!isPaused) { for (RenderWindow wind : winds.values()) { if (shutdown.get()) break; try { GLWindow glWin = wind.getGLWindowRef(); GLContext context = glWin.getContext(); if (context != null) glWin.display(); } catch (Throwable t) { // Log it, but move on to the other windows logException(t); } } } long loopEnd = System.nanoTime(); loopTimeNS = (loopEnd - lastLoopEnd); lastLoopEnd = loopEnd; try { synchronized (displayNeeded) { if (!displayNeeded.get()) { displayNeeded.wait(); } } } catch (InterruptedException e) { // Let's loop anyway... } } catch (Throwable t) { // Any other unexpected exceptions... logException(t); } } } /** * Returns the shader object for this handle, should only be called from the render thread (during a render) * @param h * @return */ public Shader getShader(ShaderHandle h) { return shaders.get(h); } public Shader getMeshShader(int flags) { assert(flags < NUM_MESH_SHADERS); return meshShaders[flags]; } /** * Returns the MeshProto for the supplied key, should only be called from the render thread (during a render) * @param key * @return */ public MeshProto getProto(MeshProtoKey key) { MeshProto proto = protoCache.get(key); if (proto == null) { // This prototype needs to be lazily loaded loadMeshProtoImp(key); } return protoCache.get(key); } public TessFont getTessFont(TessFontKey key) { if (!fontCache.containsKey(key)) { loadTessFontImp(key); // Try lazy initialization for now } return fontCache.get(key); } public void setScene(ArrayList<RenderProxy> scene) { synchronized (sceneLock) { proxyScene = scene; } } public void queueRedraw() { synchronized(displayNeeded) { displayNeeded.set(true); displayNeeded.notifyAll(); } } private void addRenderMessage(RenderMessage msg) { synchronized(renderMessages) { renderMessages.add(msg); queueRedraw(); } } public void setCameraInfoForWindow(int windowID, CameraInfo info) { addRenderMessage(new SetCameraMessage(windowID, info)); } private void setCameraInfoImp(SetCameraMessage mes) { synchronized (openWindows) { Camera cam = cameras.get(mes.windowID); if (cam == null) { // Bad windowID throw new RuntimeException("Bad window ID"); } cam.setInfo(mes.cameraInfo); } } /** * Call this from any thread to shutdown the Renderer, will return * immediately but the renderer will shutdown after the next redraw */ public void shutdown() { shutdown.set(true); queueRedraw(); } public GL2GL3 getGL() { return drawContext.getGL().getGL2GL3(); } /** * Get a list of all the IDs of currently open windows * @return */ public ArrayList<Integer> getOpenWindowIDs() { synchronized(openWindows) { ArrayList<Integer> ret = new ArrayList<>(); for (int id : openWindows.keySet()) { ret.add(id); } return ret; } } public String getWindowName(int windowID) { synchronized(openWindows) { RenderWindow win = openWindows.get(windowID); if (win == null) { return null; } return win.getName(); } } public Frame getAWTFrame(int windowID) { synchronized(openWindows) { RenderWindow win = openWindows.get(windowID); if (win == null) { return null; } return win.getAWTFrameRef(); } } public void focusWindow(int windowID) { final Frame awtRef = getAWTFrame(windowID); if (awtRef == null) return; awtRef.setExtendedState(Frame.NORMAL); awtRef.toFront(); } /** * Construct a new window (a NEWT window specifically) * * @param width * @param height * @return */ private void createWindowImp(CreateWindowMessage message) { RenderGLListener listener = new RenderGLListener(); final RenderWindow window = new RenderWindow(message.x, message.y, message.width, message.height, message.title, message.name, sharedContext, caps, listener, message.icon, message.windowID, message.viewID, message.listener); listener.setWindow(window); Camera camera = new Camera(Math.PI/3.0, 1, 0.1, 1000); synchronized (openWindows) { openWindows.put(message.windowID, window); cameras.put(message.windowID, camera); } window.getGLWindowRef().setAnimator(this); GLWindowListener wl = new GLWindowListener(window.getWindowID()); window.getGLWindowRef().addWindowListener(wl); window.getAWTFrameRef().addComponentListener(wl); window.getGLWindowRef().addMouseListener(new MouseHandler(window, message.listener)); window.getGLWindowRef().addKeyListener(message.listener); window.getAWTFrameRef().setType(Type.UTILITY); window.getAWTFrameRef().setAutoRequestFocus(false); EventQueue.invokeLater(new Runnable() { @Override public void run() { window.getAWTFrameRef().setVisible(true); } }); queueRedraw(); } public int createWindow(int x, int y, int width, int height, int viewID, String title, String name, Image icon, WindowInteractionListener listener) { int windowID = getAssetID(); addRenderMessage(new CreateWindowMessage(x, y, width, height, title, name, windowID, viewID, icon, listener)); return windowID; } public void setWindowDebugInfo(int windowID, String debugString, ArrayList<Long> debugIDs) { synchronized(openWindows) { RenderWindow w = openWindows.get(windowID); if (w != null) { w.setDebugString(debugString); w.setDebugIDs(debugIDs); } } } public void closeWindow(int windowID) { addRenderMessage(new CloseWindowMessage(windowID)); } private void closeWindowImp(CloseWindowMessage msg) { RenderWindow window; synchronized(openWindows) { window = openWindows.get(msg.windowID); if (window == null) { return; } } windowCleanup(msg.windowID); window.getGLWindowRef().destroy(); window.getAWTFrameRef().dispose(); } private String readSource(String file) { URL res = Renderer.class.getResource(file); StringBuilder source = new StringBuilder(); try { BufferedReader reader = new BufferedReader(new InputStreamReader(res.openStream())); while (true) { String line = reader.readLine(); if (line == null) break; source.append(line).append("\n"); } reader.close(); } catch (IOException e) {} return source.toString(); } private void createShader(ShaderHandle sh, String vert, String frag, GL2GL3 gl) { String vertsrc = readSource(vert); String fragsrc = readSource(frag); Shader s = new Shader(vertsrc, fragsrc, gl); if (s.isGood()) { shaders.put(sh, s); return; } String failure = s.getFailureLog(); throw new RenderException("Shader failed: " + sh.toString() + " " + failure); } private void createCoreShader(ShaderHandle sh, String vert, String frag, GL2GL3 gl, String version) { String vertsrc = readSource(vert).replaceAll("@VERSION@", version); String fragsrc = readSource(frag).replaceAll("@VERSION@", version); Shader s = new Shader(vertsrc, fragsrc, gl); if (s.isGood()) { shaders.put(sh, s); return; } String failure = s.getFailureLog(); throw new RenderException("Shader failed: " + sh.toString() + " " + failure); } private String getMeshShaderDefines(int i) { StringBuilder defines = new StringBuilder(); if ((i & DIFF_TEX_FLAG) != 0) { defines.append("#define DIFF_TEX\n"); } return defines.toString(); } private static final Pattern definespat = Pattern.compile("@DEFINES@"); /** * Create and compile all the shaders */ private void initShaders(GL2GL3 gl) throws RenderException { shaders = new EnumMap<>(ShaderHandle.class); String vert, frag; vert = "/resources/shaders/font.vert"; frag = "/resources/shaders/font.frag"; createShader(ShaderHandle.FONT, vert, frag, gl); vert = "/resources/shaders/hull.vert"; frag = "/resources/shaders/hull.frag"; createShader(ShaderHandle.HULL, vert, frag, gl); vert = "/resources/shaders/overlay-font.vert"; frag = "/resources/shaders/overlay-font.frag"; createShader(ShaderHandle.OVERLAY_FONT, vert, frag, gl); vert = "/resources/shaders/overlay-flat.vert"; frag = "/resources/shaders/overlay-flat.frag"; createShader(ShaderHandle.OVERLAY_FLAT, vert, frag, gl); vert = "/resources/shaders/debug.vert"; frag = "/resources/shaders/debug.frag"; createShader(ShaderHandle.DEBUG, vert, frag, gl); vert = "/resources/shaders/skybox.vert"; frag = "/resources/shaders/skybox.frag"; createShader(ShaderHandle.SKYBOX, vert, frag, gl); String meshVertSrc = readSource("/resources/shaders/flat.vert"); String meshFragSrc = readSource("/resources/shaders/flat.frag"); // Create the mesh shaders for (int i = 0; i < NUM_MESH_SHADERS; ++i) { String defines = getMeshShaderDefines(i); String definedFragSrc = definespat.matcher(meshFragSrc).replaceAll(defines); Shader s = new Shader(meshVertSrc, definedFragSrc, gl); if (!s.isGood()) { String failure = s.getFailureLog(); throw new RenderException("Mesh Shader failed, flags: " + i + " " + failure); } meshShaders[i] = s; } } /** * Common code to setup basic openGL state, including depth test, blending etc. * @param gl */ private void initSurfaceState(GL2GL3 gl) { // Some of this is probably redundant, but here goes gl.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); gl.glEnable(GL.GL_DEPTH_TEST); gl.glClearDepth(1.0); gl.glDepthFunc(GL2GL3.GL_LEQUAL); gl.glEnable(GL2GL3.GL_CULL_FACE); gl.glCullFace(GL2GL3.GL_BACK); gl.glBlendEquationSeparate(GL2GL3.GL_FUNC_ADD, GL2GL3.GL_MAX); gl.glBlendFuncSeparate(GL2GL3.GL_SRC_ALPHA, GL2GL3.GL_ONE_MINUS_SRC_ALPHA, GL2GL3.GL_ONE, GL2GL3.GL_ONE); } /** * Create and compile all the shaders */ private void initCoreShaders(GL2GL3 gl, String version) throws RenderException { shaders = new EnumMap<>(ShaderHandle.class); String vert, frag; vert = "/resources/shaders_core/font.vert"; frag = "/resources/shaders_core/font.frag"; createCoreShader(ShaderHandle.FONT, vert, frag, gl, version); vert = "/resources/shaders_core/hull.vert"; frag = "/resources/shaders_core/hull.frag"; createCoreShader(ShaderHandle.HULL, vert, frag, gl, version); vert = "/resources/shaders_core/overlay-font.vert"; frag = "/resources/shaders_core/overlay-font.frag"; createCoreShader(ShaderHandle.OVERLAY_FONT, vert, frag, gl, version); vert = "/resources/shaders_core/overlay-flat.vert"; frag = "/resources/shaders_core/overlay-flat.frag"; createCoreShader(ShaderHandle.OVERLAY_FLAT, vert, frag, gl, version); vert = "/resources/shaders_core/debug.vert"; frag = "/resources/shaders_core/debug.frag"; createCoreShader(ShaderHandle.DEBUG, vert, frag, gl, version); vert = "/resources/shaders_core/skybox.vert"; frag = "/resources/shaders_core/skybox.frag"; createCoreShader(ShaderHandle.SKYBOX, vert, frag, gl, version); String meshVertSrc = readSource("/resources/shaders_core/flat.vert").replaceAll("@VERSION@", version); String meshFragSrc = readSource("/resources/shaders_core/flat.frag").replaceAll("@VERSION@", version); // Create the mesh shaders for (int i = 0; i < NUM_MESH_SHADERS; ++i) { String defines = getMeshShaderDefines(i); String definedFragSrc = definespat.matcher(meshFragSrc).replaceAll(defines); Shader s = new Shader(meshVertSrc, definedFragSrc, gl); if (!s.isGood()) { String failure = s.getFailureLog(); throw new RenderException("Mesh Shader failed, flags: " + i + " " + failure); } meshShaders[i] = s; } } /** * Basic message dispatch * * @param message */ private void handleMessage(RenderMessage message) { assert (Thread.currentThread() == renderThread); if (message instanceof CreateWindowMessage) { createWindowImp((CreateWindowMessage) message); return; } if (message instanceof SetCameraMessage) { setCameraInfoImp((SetCameraMessage) message); return; } if (message instanceof OffScreenMessage) { offScreenImp((OffScreenMessage) message); return; } if (message instanceof CloseWindowMessage) { closeWindowImp((CloseWindowMessage) message); return; } if (message instanceof CreateOffscreenTargetMessage) { populateOffscreenTarget(((CreateOffscreenTargetMessage)message).target); } if (message instanceof FreeOffscreenTargetMessage) { freeOffscreenTargetImp(((FreeOffscreenTargetMessage)message).target); } } private void initSharedContext() { assert (Thread.currentThread() == renderThread); assert (drawContext == null); int res = sharedContext.makeCurrent(); assert (res == GLContext.CONTEXT_CURRENT); if (USE_DEBUG_GL) { sharedContext.setGL(new DebugGL4bc((GL4bc)sharedContext.getGL().getGL2GL3())); } LogBox.formatRenderLog("Found OpenGL version: %s", sharedContext.getGLVersion()); LogBox.formatRenderLog("Found GLSL: %s", sharedContext.getGLSLVersionString()); VersionNumber vn = sharedContext.getGLVersionNumber(); boolean isCore = sharedContext.isGLCoreProfile(); LogBox.formatRenderLog("OpenGL Major: %d Minor: %d IsCore:%s", vn.getMajor(), vn.getMinor(), isCore); if (vn.getMajor() < 2) { throw new RenderException("OpenGL version is too low. OpenGL >= 2.1 is required."); } GL2GL3 gl = sharedContext.getGL().getGL2GL3(); if (!isCore) initShaders(gl); else initCoreShaders(gl, sharedContext.getGLSLVersionString()); // Sub system specific intitializations DebugUtils.init(this, gl); Polygon.init(this, gl); MeshProto.init(this, gl); texCache.init(gl); // Load the bad mesh proto badData = MeshDataCache.getBadMesh(); badProto = new MeshProto(badData, safeGraphics); badProto.loadGPUAssets(gl, this); skybox = new Skybox(); sharedContext.release(); } private void loadMeshProtoImp(final MeshProtoKey key) { //long startNanos = System.nanoTime(); assert(drawContext == null || !drawContext.isCurrent()); if (protoCache.get(key) != null) { return; // This mesh has already been loaded } int res = sharedContext.makeCurrent(); assert (res == GLContext.CONTEXT_CURRENT); GL2GL3 gl = sharedContext.getGL().getGL2GL3(); MeshProto proto; MeshData data = MeshDataCache.getMeshData(key); if (data == badData) { proto = badProto; } else { proto = new MeshProto(data, safeGraphics); assert (proto != null); proto.loadGPUAssets(gl, this); if (!proto.isLoadedGPU()) { // This did not load cleanly, clear it out and use the default bad mesh asset proto.freeResources(gl); LogBox.formatRenderLog("Could not load GPU assset: %s\n", key.getURI().toString()); proto = badProto; } } protoCache.put(key, proto); sharedContext.release(); // long endNanos = System.nanoTime(); // long ms = (endNanos - startNanos) /1000000L; // LogBox.formatRenderLog("LoadMeshProtoImp time:" + ms + "ms"); } private void loadTessFontImp(TessFontKey key) { if (fontCache.get(key) != null) { return; // This font has already been loaded } TessFont tf = new TessFont(key); fontCache.put(key, tf); } public int generateVAO(int contextID, GL2GL3 gl) { assert(Thread.currentThread() == renderThread); int[] vaos = new int[1]; gl.glGenVertexArrays(1, vaos, 0); int vao = vaos[0]; synchronized(openWindows) { RenderWindow wind = openWindows.get(contextID); if (wind != null) { wind.addVAO(vao); } } return vao; } // Recreate the internal scene based on external input private void updateRenderableScene() { synchronized (sceneLock) { long sceneStart = System.nanoTime(); currentScene = new ArrayList<>(); currentOverlay = new ArrayList<>(); for (RenderProxy proxy : proxyScene) { proxy.collectRenderables(this, currentScene); proxy.collectOverlayRenderables(this, currentOverlay); } sceneTimeNS = System.nanoTime() - sceneStart; } } public static class PickResult { public double dist; public long pickingID; public PickResult(double dist, long pickingID) { this.dist = dist; this.pickingID = pickingID; } } /** * Cast the provided ray into the current scene and return the list of bounds collisions * @param ray * @return */ public List<PickResult> pick(Ray pickRay, int viewID, boolean precise) { synchronized(openWindows) { ArrayList<PickResult> ret = new ArrayList<>(); if (currentScene == null) { return ret; } Camera cam = null; for (RenderWindow wind : openWindows.values()) { if (wind.getViewID() == viewID) { cam = cameras.get(wind.getWindowID()); break; } } if (cam == null) { // Invalid view return ret; } // Do not update the scene while a pick is underway synchronized (sceneLock) { for (Renderable r : currentScene) { double rayDist = r.getCollisionDist(pickRay, precise); if (rayDist >= 0.0) { if (r.renderForView(viewID, cam)) { ret.add(new PickResult(rayDist, r.getPickingID())); } } } return ret; } } } public static class WindowMouseInfo { public int x, y; public int width, height; public int viewableX, viewableY; public boolean mouseInWindow; public CameraInfo cameraInfo; } /** * Get Window specific information about the mouse. This is very useful for picking on the App side * @param windowID * @return */ public WindowMouseInfo getMouseInfo(int windowID) { synchronized(openWindows) { RenderWindow w = openWindows.get(windowID); if (w == null) { return null; // Not a valid window ID, or the window has closed } WindowMouseInfo info = new WindowMouseInfo(); info.x = w.getMouseX(); info.y = w.getMouseY(); info.width = w.getViewableWidth(); info.height = w.getViewableHeight(); info.viewableX = w.getViewableX(); info.viewableY = w.getViewableY(); info.mouseInWindow = w.isMouseInWindow(); info.cameraInfo = cameras.get(windowID).getInfo(); return info; } } public CameraInfo getCameraInfo(int windowID) { synchronized(openWindows) { Camera cam = cameras.get(windowID); if (cam == null) { return null; // Not a valid window ID } return cam.getInfo(); } } // Common cleanup code for window closing. Applies to both user closed and programatically closed windows private void windowCleanup(int windowID) { RenderWindow w; synchronized(openWindows) { w = openWindows.get(windowID); if (w == null) { return; } openWindows.remove(windowID); } w.getAWTFrameRef().setVisible(false); w.getGLWindowRef().destroy(); // Fire the window closing callback w.getWindowListener().windowClosing(); } private class GLWindowListener implements WindowListener, ComponentListener { private final int windowID; public GLWindowListener(int id) { windowID = id; } private WindowInteractionListener getListener() { synchronized(openWindows) { RenderWindow w = openWindows.get(windowID); if (w == null) { return null; // Not a valid window ID, or the window has closed } return w.getWindowListener(); } } @Override public void windowDestroyNotify(WindowEvent we) { windowCleanup(windowID); } @Override public void windowDestroyed(WindowEvent arg0) { } @Override public void windowGainedFocus(WindowEvent arg0) { WindowInteractionListener listener = getListener(); if (listener != null) { listener.windowGainedFocus(); } } @Override public void windowLostFocus(WindowEvent arg0) { } @Override public void windowMoved(WindowEvent arg0) { } @Override public void windowRepaint(WindowUpdateEvent arg0) { } @Override public void windowResized(WindowEvent arg0) { } private void updateWindowSizeAndPos() { RenderWindow w; synchronized(openWindows) { w = openWindows.get(windowID); if (w == null) { return; } } w.getWindowListener().windowMoved(w.getWindowX(), w.getWindowY(), w.getWindowWidth(), w.getWindowHeight()); } @Override public void componentHidden(ComponentEvent arg0) { } @Override public void componentMoved(ComponentEvent arg0) { updateWindowSizeAndPos(); } @Override public void componentResized(ComponentEvent arg0) { updateWindowSizeAndPos(); } @Override public void componentShown(ComponentEvent arg0) { } } private class RenderGLListener implements GLEventListener { private RenderWindow window; private long lastFrameNanos = 0; public void setWindow(RenderWindow win) { window = win; } @Override public void init(GLAutoDrawable drawable) { synchronized (rendererLock) { // Per window initialization if (USE_DEBUG_GL) { drawable.setGL(new DebugGL4bc((GL4bc)drawable.getGL().getGL2GL3())); } GL2GL3 gl = drawable.getGL().getGL2GL3(); initSurfaceState(gl); gl.glEnable(GL.GL_MULTISAMPLE); } } @Override public void dispose(GLAutoDrawable drawable) { synchronized (rendererLock) { GL2GL3 gl = drawable.getGL().getGL2GL3(); ArrayList<Integer> vaoArray = window.getVAOs(); int[] vaos = new int[vaoArray.size()]; int index = 0; for (int vao : vaoArray) { vaos[index++] = vao; } if (vaos.length > 0) { gl.glDeleteVertexArrays(vaos.length, vaos, 0); } } } @Override public void display(GLAutoDrawable drawable) { synchronized (rendererLock) { Camera cam = cameras.get(window.getWindowID()); // The ray of the current mouse position (or null if the mouse is not hovering over the window) Ray pickRay = RenderUtils.getPickRay(getMouseInfo(window.getWindowID())); PerfInfo pi = new PerfInfo(); long startNanos = System.nanoTime(); allowDelayedTextures = true; // Cache the current scene. This way we don't need to lock it for the full render ArrayList<Renderable> scene = new ArrayList<>(currentScene.size()); ArrayList<OverlayRenderable> overlay = new ArrayList<>(currentOverlay.size()); synchronized(sceneLock) { scene.addAll(currentScene); overlay.addAll(currentOverlay); } renderScene(drawable.getContext(), window.getWindowID(), scene, overlay, cam, window.getViewableWidth(), window.getViewableHeight(), pickRay, window.getViewID(), pi); GL2GL3 gl = drawable.getContext().getGL().getGL2GL3(); // Just to clean up the code below boolean showDebug; synchronized(settingsLock) { showDebug = showDebugInfo; } if (showDebug) { // Draw a window specific performance counter gl.glDisable(GL2GL3.GL_DEPTH_TEST); drawContext = drawable.getContext(); StringBuilder perf = new StringBuilder(); perf.append( String.format( "Objects Culled: %s", pi.objectsCulled) ); perf.append( String.format( " VRAM (MB): %.0f", usedVRAM/(1024.0*1024.0)) ); perf.append( String.format( " Frame time (ms): %.3f", lastFrameNanos/1000000.0) ); perf.append( String.format( " SceneTime (ms): %.3f", sceneTimeNS/1000000.0) ); perf.append( String.format( " Loop Time (ms): %.3f", loopTimeNS/1000000.0) ); TessFont defFont = getTessFont(defaultBoldFontKey); OverlayString os = new OverlayString(defFont, perf.toString(), ColourInput.BLACK, 10, 10, 15, false, false, DisplayModel.ALWAYS); os.render(window.getWindowID(), Renderer.this, window.getViewableWidth(), window.getViewableHeight(), cam, null); // Also draw this window's debug string os = new OverlayString(defFont, window.getDebugString(), ColourInput.BLACK, 10, 10, 30, false, false, DisplayModel.ALWAYS); os.render(window.getWindowID(), Renderer.this, window.getViewableWidth(), window.getViewableHeight(), cam, null); drawContext = null; gl.glEnable(GL2GL3.GL_DEPTH_TEST); } gl.glFinish(); long endNanos = System.nanoTime(); lastFrameNanos = endNanos - startNanos; } } @Override public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) { //_window.resized(width, height); Camera cam = cameras.get(window.getWindowID()); cam.setAspectRatio((double) width / (double) height); } } /** * Abstract base type for internal renderer messages */ private static class RenderMessage { @SuppressWarnings("unused") public long queueTime = System.nanoTime(); } private static class CreateWindowMessage extends RenderMessage { public int x, y; public int width, height; public String title, name; public WindowInteractionListener listener; public int windowID, viewID; public Image icon; public CreateWindowMessage(int x, int y, int width, int height, String title, String name, int windowID, int viewID, Image icon, WindowInteractionListener listener) { this.x = x; this.y = y; this.width = width; this.height = height; this.title = title; this.name = name; this.listener = listener; this.windowID = windowID; this.viewID = viewID; this.icon = icon; } } private static class SetCameraMessage extends RenderMessage { public int windowID; public CameraInfo cameraInfo; public SetCameraMessage(int windowID, CameraInfo cameraInfo) { this.windowID = windowID; this.cameraInfo = cameraInfo; } } private static class OffScreenMessage extends RenderMessage { public ArrayList<RenderProxy> scene; public int viewID; public Camera cam; public int width, height; public Future<BufferedImage> result; public OffscreenTarget target; OffScreenMessage(ArrayList<RenderProxy> s, int vID, Camera c, int w, int h, Future<BufferedImage> r, OffscreenTarget t) { scene = s; viewID = vID; cam = c; width = w; height = h; result = r; target = t; } } private static class CloseWindowMessage extends RenderMessage { public int windowID; public CloseWindowMessage(int id) { windowID = id; } } private static class CreateOffscreenTargetMessage extends RenderMessage { public OffscreenTarget target; CreateOffscreenTargetMessage(OffscreenTarget t) { target = t; } } private static class FreeOffscreenTargetMessage extends RenderMessage { public OffscreenTarget target; FreeOffscreenTargetMessage(OffscreenTarget t) { target = t; } } public TexCache getTexCache() { return texCache; } public static boolean debugDrawHulls() { return false; } public static boolean debugDrawAABBs() { return false; } public static boolean debugDrawArmatures() { return false; } public boolean isInitialized() { return initialized.get() && !fatalError.get(); } public boolean isGL3Supported() { return gl3Supported; } public boolean hasFatalError() { return fatalError.get(); } public String getErrorString() { return errorString; } public StackTraceElement[] getFatalStackTrace() { return fatalStackTrace; } public TessFontKey getDefaultFont() { return defaultFontKey; } public boolean allowDelayedTextures() { return allowDelayedTextures; } private void logException(Throwable t) { exceptionLogger.logException(t); // For now print a synopsis for all exceptions thrown printExceptionLog(); LogBox.renderLogException(t); } private void printExceptionLog() { LogBox.renderLog("Exceptions from Renderer: "); exceptionLogger.printExceptionLog(); LogBox.renderLog(""); } /** * Queue up an off screen rendering * @param scene * @param cam * @param width * @param height * @return */ public Future<BufferedImage> renderOffscreen(ArrayList<RenderProxy> scene, int viewID, CameraInfo camInfo, int width, int height, Runnable runWhenDone, OffscreenTarget target) { Future<BufferedImage> result = new Future<>(runWhenDone); Camera cam = new Camera(camInfo, (double)width/(double)height); addRenderMessage(new OffScreenMessage(scene, viewID, cam, width, height, result, target)); return result; } public OffscreenTarget createOffscreenTarget(int width, int height) { OffscreenTarget ret = new OffscreenTarget(width, height); addRenderMessage(new CreateOffscreenTargetMessage(ret)); return ret; } public void freeOffscreenTarget(OffscreenTarget target) { addRenderMessage(new FreeOffscreenTargetMessage(target)); } /** * Create the resources for an OffscreenTarget */ private void populateOffscreenTarget(OffscreenTarget target) { int width = target.getWidth(); int height = target.getHeight(); sharedContext.makeCurrent(); GL3 gl = sharedContext.getGL().getGL3(); // Just to clean up the code below // This does not support opengl 3, so for now we don't support off screen rendering if (gl == null) { sharedContext.release(); return; } // Create a new frame buffer for this draw operation int[] temp = new int[2]; gl.glGenFramebuffers(2, temp, 0); int drawFBO = temp[0]; int blitFBO = temp[1]; gl.glGenTextures(2, temp, 0); int drawTex = temp[0]; int blitTex = temp[1]; gl.glGenRenderbuffers(1, temp, 0); int depthBuf = temp[0]; gl.glBindTexture(GL3.GL_TEXTURE_2D_MULTISAMPLE, drawTex); gl.glTexImage2DMultisample(GL3.GL_TEXTURE_2D_MULTISAMPLE, 4, GL2GL3.GL_RGBA8, width, height, true); gl.glBindRenderbuffer(GL2GL3.GL_RENDERBUFFER, depthBuf); gl.glRenderbufferStorageMultisample(GL2GL3.GL_RENDERBUFFER, 4, GL2GL3.GL_DEPTH_COMPONENT, width, height); gl.glBindFramebuffer(GL2GL3.GL_FRAMEBUFFER, drawFBO); gl.glFramebufferTexture2D(GL2GL3.GL_FRAMEBUFFER, GL2GL3.GL_COLOR_ATTACHMENT0, GL3.GL_TEXTURE_2D_MULTISAMPLE, drawTex, 0); gl.glFramebufferRenderbuffer(GL2GL3.GL_FRAMEBUFFER, GL2GL3.GL_DEPTH_ATTACHMENT, GL2GL3.GL_RENDERBUFFER, depthBuf); int fbStatus = gl.glCheckFramebufferStatus(GL2GL3.GL_FRAMEBUFFER); assert(fbStatus == GL2GL3.GL_FRAMEBUFFER_COMPLETE); gl.glBindTexture(GL2GL3.GL_TEXTURE_2D, blitTex); gl.glTexImage2D(GL2GL3.GL_TEXTURE_2D, 0, GL2GL3.GL_RGBA8, width, height, 0, GL2GL3.GL_RGBA, GL2GL3.GL_BYTE, null); gl.glBindFramebuffer(GL2GL3.GL_FRAMEBUFFER, blitFBO); gl.glFramebufferTexture2D(GL2GL3.GL_FRAMEBUFFER, GL2GL3.GL_COLOR_ATTACHMENT0, GL2GL3.GL_TEXTURE_2D, blitTex, 0); gl.glBindFramebuffer(GL2GL3.GL_FRAMEBUFFER, 0); target.load(drawFBO, drawTex, depthBuf, blitFBO, blitTex); sharedContext.release(); } private void freeOffscreenTargetImp(OffscreenTarget target) { if (!target.isLoaded()) { return; // Nothing to free } sharedContext.makeCurrent(); GL2GL3 gl = sharedContext.getGL().getGL2GL3(); // Just to clean up the code below int[] temp = new int[2]; temp[0] = target.getDrawFBO(); temp[1] = target.getBlitFBO(); gl.glDeleteFramebuffers(2, temp, 0); temp[0] = target.getDrawTex(); temp[1] = target.getBlitTex(); gl.glDeleteTextures(2, temp, 0); temp[0] = target.getDepthBuffer(); gl.glDeleteRenderbuffers(1, temp, 0); target.free(); sharedContext.release(); } private void offScreenImp(OffScreenMessage message) { synchronized(rendererLock) { try { boolean isTempTarget; OffscreenTarget target; if (message.target == null) { isTempTarget = true; target = new OffscreenTarget(message.width, message.height); populateOffscreenTarget(target); } else { isTempTarget = false; target = message.target; assert(target.getWidth() == message.width); assert(target.getHeight() == message.height); } int width = message.width; int height = message.height; if (!target.isLoaded()) { message.result.setFailed("Context not loaded. Is OpenGL 3 supported?"); return; } assert(target.isLoaded()); // Collect the renderables final ArrayList<Renderable> renderables; ArrayList<OverlayRenderable> overlay; if (message.scene != null) { renderables = new ArrayList<>(); overlay = new ArrayList<>(); for (RenderProxy p : message.scene) { p.collectRenderables(this, renderables); p.collectOverlayRenderables(this, overlay); } } else { // Use the current current scene if one is not provided synchronized(sceneLock) { renderables = new ArrayList<>(currentScene); overlay = new ArrayList<>(currentOverlay); } } sharedContext.makeCurrent(); GL2GL3 gl = sharedContext.getGL().getGL2GL3(); // Just to clean up the code below gl.glBindFramebuffer(GL2GL3.GL_DRAW_FRAMEBUFFER, target.getDrawFBO()); gl.glViewport(0, 0, width, height); initSurfaceState(gl); allowDelayedTextures = false; PerfInfo perfInfo = new PerfInfo(); // Okay, now actually render this thing... renderScene(sharedContext, sharedContextID, renderables, overlay, message.cam, width, height, null, message.viewID, perfInfo); gl.glFinish(); gl.glBindFramebuffer(GL2GL3.GL_DRAW_FRAMEBUFFER, target.getBlitFBO()); gl.glBindFramebuffer(GL2GL3.GL_READ_FRAMEBUFFER, target.getDrawFBO()); gl.glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL2GL3.GL_COLOR_BUFFER_BIT, GL2GL3.GL_NEAREST); gl.glBindTexture(GL2GL3.GL_TEXTURE_2D, target.getBlitTex()); IntBuffer pixels = target.getPixelBuffer(); gl.glGetTexImage(GL2GL3.GL_TEXTURE_2D, 0, GL2GL3.GL_BGRA, GL2GL3.GL_UNSIGNED_INT_8_8_8_8_REV, pixels); gl.glBindTexture(GL2GL3.GL_TEXTURE_2D, 0); BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); for (int h = 0; h < height; ++h) { // Set this one scan line at a time, in the opposite order as java is y down img.setRGB(0, h, width, 1, pixels.array(), (height - 1 - h) * width, width); } message.result.setComplete(img); // Clean up gl.glBindFramebuffer(GL2GL3.GL_READ_FRAMEBUFFER, 0); gl.glBindFramebuffer(GL2GL3.GL_DRAW_FRAMEBUFFER, 0); if (isTempTarget) { freeOffscreenTargetImp(target); } } catch (GLException ex){ message.result.setFailed(ex.getMessage()); } finally { if (sharedContext.isCurrent()) sharedContext.release(); } } // synchronized(_rendererLock) } /** * Returns true if the current thread is this renderer's render thread * @return */ public boolean isRenderThread() { return (Thread.currentThread() == renderThread); } private static class PerfInfo { public int objectsCulled = 0; } private static class TransSortable implements Comparable<TransSortable> { public Renderable r; public double dist; @Override public int compareTo(TransSortable o) { // Sort such that largest distance sorts to front of list // by reversing argument order in compare. return Double.compare(o.dist, this.dist); } } private void renderScene(GLContext context, int contextID, List<Renderable> scene, List<OverlayRenderable> overlay, Camera cam, int width, int height, Ray pickRay, int viewID, PerfInfo perfInfo) { final Vec4d viewDir = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d); cam.getViewDir(viewDir); final Vec3d temp = new Vec3d(); assert (drawContext == null); drawContext = context; GL2GL3 gl = drawContext.getGL().getGL2GL3(); // Just to clean up the code below gl.glClear(GL2GL3.GL_COLOR_BUFFER_BIT | GL2GL3.GL_DEPTH_BUFFER_BIT); // The 'height' of a pixel 1 unit from the viewer double unitPixelHeight = 2 * Math.tan(cam.getFOV()/2.0) / height; ArrayList<TransSortable> transparents = new ArrayList<>(); if (scene == null) return; for (Renderable r : scene) { AABB bounds = r.getBoundsRef(); double dist = cam.distToBounds(bounds); if (!r.renderForView(viewID, cam)) { continue; } if (!cam.collides(bounds)) { ++perfInfo.objectsCulled; continue; } double apparentSize = 2 * bounds.radius.mag3() / Math.abs(dist); if (apparentSize < unitPixelHeight) { // This object is too small to draw ++perfInfo.objectsCulled; continue; } if (r.hasTransparent()) { // Defer rendering of transparent objects TransSortable ts = new TransSortable(); ts.r = r; temp.set3(r.getBoundsRef().center); temp.sub3(cam.getTransformRef().getTransRef()); ts.dist = temp.dot3(viewDir); transparents.add(ts); } r.render(contextID, this, cam, pickRay); } gl.glEnable(GL2GL3.GL_BLEND); gl.glDepthMask(false); // Draw the skybox after skybox.setTexture(cam.getInfoRef().skyboxTexture); skybox.render(contextID, this, cam); Collections.sort(transparents); for (TransSortable ts : transparents) { AABB bounds = ts.r.getBoundsRef(); if (!cam.collides(bounds)) { ++perfInfo.objectsCulled; continue; } ts.r.renderTransparent(contextID, this, cam, pickRay); } gl.glDisable(GL2GL3.GL_BLEND); gl.glDepthMask(true); // Debug render AABBs if (debugDrawAABBs()) { Color4d yellow = new Color4d(1, 1, 0, 1.0d); Color4d red = new Color4d(1, 0, 0, 1.0d); for (Renderable r : scene) { Color4d aabbColor = yellow; if (pickRay != null && r.getBoundsRef().collisionDist(pickRay) > 0) { aabbColor = red; } DebugUtils.renderAABB(contextID, this, r.getBoundsRef(), aabbColor, cam); } } // for renderables // Now draw the overlay gl.glDisable(GL2GL3.GL_DEPTH_TEST); if (overlay != null) { for (OverlayRenderable r : overlay) { if (!r.renderForView(viewID, cam)) { continue; } r.render(contextID, this, width, height, cam, pickRay); } } gl.glEnable(GL2GL3.GL_DEPTH_TEST); gl.glBindVertexArray(0); drawContext = null; } public void usingVRAM(long bytes) { usedVRAM += bytes; } ///////////////////////////////////////////////////////////////////// // Below are functions inherited from GLAnimatorControl // the interface is a bit of a mess and there's a lot here we don't need @Override public long getFPSStartTime() { // Not supported assert(false); return 0; } @Override public float getLastFPS() { // Not supported return 0; } @Override public long getLastFPSPeriod() { // Not supported return 0; } @Override public long getLastFPSUpdateTime() { // Not supported return 0; } @Override public float getTotalFPS() { // Not supported return 0; } @Override public long getTotalFPSDuration() { // Not supported return 0; } @Override public int getTotalFPSFrames() { // Not supported return 0; } @Override public int getUpdateFPSFrames() { // Not supported return 0; } @Override public void resetFPSCounter() { // Not supported } @Override public void setUpdateFPSFrames(int arg0, PrintStream arg1) { // Not supported } @Override public void add(GLAutoDrawable arg0) { // Not supported assert(false); } @Override public Thread getThread() { return renderThread; } @Override public boolean isAnimating() { queueRedraw(); return true; } @Override public boolean isPaused() { synchronized (rendererLock) { // Make sure we aren't currently rendering return isPaused; } } @Override public boolean isStarted() { return true; } @Override public boolean pause() { synchronized(rendererLock) { isPaused = true; return true; } } @Override public void remove(GLAutoDrawable arg0) { // Not supported assert(false); } @Override public boolean resume() { isPaused = false; queueRedraw(); return true; } @Override public boolean start() { // Not supported assert(false); return false; } @Override public boolean stop() { // Not supported assert(false); return false; } private void checkForIntelDriver() { int res = sharedContext.makeCurrent(); assert (res == GLContext.CONTEXT_CURRENT); GL2GL3 gl = sharedContext.getGL().getGL2GL3(); String vendorString = gl.glGetString(GL2GL3.GL_VENDOR).toLowerCase(); boolean intelDriver = vendorString.indexOf("intel") != -1; if (intelDriver) { safeGraphics = true; } sharedContext.release(); } public void setDebugInfo(boolean showDebug) { synchronized(settingsLock) { showDebugInfo = showDebug; } } @Override public UncaughtExceptionHandler getUncaughtExceptionHandler() { return null; } @Override public void setUncaughtExceptionHandler(UncaughtExceptionHandler arg0) {} }