// SDL2_14 [OpenGL ES Gears].nova // Interactive OpenGL ES gears. /* * Copyright (C) 1999-2001 Brian Paul All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * BRIAN PAUL BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * Ported to GLES2. * Kristian Høgsberg * May 3, 2010 * * Improve GLES2 port: * * Refactor gear drawing. * * Use correct normals for surfaces. * * Improve shader. * * Use perspective projection transformation. * * Add FPS count. * * Add comments. * Alexandros Frantzis * Jul 13, 2010 */ /* * Converted to C++ / Nova / SDL2 / Emscripten. * Robert Platt * 29th October 2023 */ // Using namespace declarations. using namespace lib.emscripten; using namespace lib.math; using namespace lib.opengl; using namespace lib.sdl2; // The application class. class SDL2_14_OpenGL_ES_Gears { // Static data members. private static SDL_Window w; private static SDL_Renderer r; private static SDL_GLContext c; private static int initialSizeX, initialSizeY; private static int sizeX, sizeY; private static int prevSizeX, prevSizeY; // Used for emscripten full-window mode. private static bool done; private static bool emscriptenActive; private static DisplayMode displayMode; private static SDL_Area prevWindowSize; // Used for the desktop app full-screen mode. private static uint vertShader; private static uint fragShader; private static uint shaderProgram; private static int modelViewProjectionMatrix_location; private static int normalMatrix_location; private static int lightSourcePosition_location; private static int materialColor_location; private static float[] projectionMatrix; // The view rotation [ x, y, z ]. private static float[] view_rot; private static int mouseX, mouseY; private static bool mouseButtonDown; private static bool spinPaused; private static bool stopGearsTurning; private static float[] spinMatrix; private static float[] velocityMatrix; // The current gear rotation angle. private static float angle; private static int frames; private static double tRot0, tRate0; private static byte[] keyboardState; // The gears. private static Gear gear1, gear2, gear3; // Display mode enumeration. public enum DisplayMode : byte { Canvas, FullWindow, FullScreen } // Application class's "main" function. public static void main( String[] args ) { Stream.writeLine( "SDL2_14 [OpenGL ES Gears].nova" ); // Initialise the class's data members. initialSizeX = 800; initialSizeY = 600; sizeX = initialSizeX; sizeY = initialSizeY; prevSizeX = 0; prevSizeY = 0; done = false; frames = 0; tRot0 = -1.0; tRate0 = -1.0; emscriptenActive = Emscripten.isActive( ); displayMode = emscriptenActive ? DisplayMode.Canvas : DisplayMode.FullWindow; spinPaused = false; stopGearsTurning = false; // Output the keyboard contols. Stream.write( "\nControls:\n" + " drag pointer / swipe screen = spin gears\n" + " left click / single tap = stop spinning\n" + " double click / double tap = toggle fullscreen / full window mode\n" + " mouse wheel / scroll = rotate z-axis +ve/-ve\n" + " left = rotate y-axis +ve\n" + " right = rotate y-axis -ve\n" + " up = rotate x-axis +ve\n" + " down = rotate x-axis -ve\n" + " z = rotate z-axis +ve\n" + " shift + z = rotate z-axis -ve\n" + " s = stop / start gears turning\n" + " space = pause / resume spinning\n" + " r = reset the view\n" + " f = toggle full screen mode\n" + " w = toggle full window mode (emscripten only)\n" + " esc = exit full screen / window mode\n\n" ); // Disable the resize control. Emscripten.disableResizeControl( ); // Show the cursor in full screen mode. Emscripten.setHideMousePointerCheckbox( false ); // Set the full screen mode callback. Emscripten.setFullScreenChangeCallback( "SDL2_14_OpenGL_ES_Gears", fullScreenModeChange ); // Set the resize event callback. Emscripten.setResizeEventCallback( "SDL2_14_OpenGL_ES_Gears", resizeEventCallback ); // Setup SDL. SDL2.SDL_Init( SDL2.SDL_INIT_VIDEO ); SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_CONTEXT_MAJOR_VERSION, 2 ); SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_CONTEXT_MINOR_VERSION, 0 ); SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_DOUBLEBUFFER, 1 ); SDL2.SDL_GL_SetAttribute( SDL2.SDL_GL_DEPTH_SIZE, 24 ); w = SDL2.SDL_CreateWindow( "SDL2_14_OpenGL_ES_Gears", SDL2.SDL_WINDOWPOS_CENTERED, SDL2.SDL_WINDOWPOS_CENTERED, (int)sizeX, (int)sizeY, SDL2.SDL_WINDOW_OPENGL | SDL2.SDL_WINDOW_RESIZABLE ); // Check for a null reference. if ( w == null ) { // Output an error message. Stream.writeLine( "Failed to create window: " + SDL2.SDL_GetError( ) ); // Abort the application. return; } r = SDL2.SDL_CreateRenderer( w, -1, SDL2.SDL_RENDERER_ACCELERATED | SDL2.SDL_RENDERER_PRESENTVSYNC ); c = SDL2.SDL_GL_CreateContext( w ); // Use vertical sync (interval = 1). if ( SDL2.SDL_GL_SetSwapInterval( 1 ) == -1 ) { // Output an error message. Stream.writeLine( "Call to 'SDL_GL_SetSwapInterval' failed: " + SDL2.SDL_GetError( ) ); } keyboardState = SDL2.SDL_GetKeyboardState( ); gearsInit( ); gearsReshape( ); // Check for the emscripten environment. if ( emscriptenActive ) { int simulate_infinite_loop = 1; // Call the function repeatedly. int fps = -1; // Call the function as fast as the browser wants to render (typically 60fps). Emscripten.setMainLoop( renderFrame, fps, simulate_infinite_loop ); } else { // Rendering and event processing loop. do { renderFrame( ); } while( !done ); } // Shutdown app. OpenGL.glDeleteProgram( shaderProgram ); OpenGL.glDeleteShader( vertShader ); OpenGL.glDeleteShader( fragShader ); gear1.deleteGear( ); gear2.deleteGear( ); gear3.deleteGear( ); SDL2.SDL_GL_DeleteContext( c ); SDL2.SDL_DestroyRenderer( r ); SDL2.SDL_DestroyWindow( w ); SDL2.SDL_Quit( ); } // Emscripten full screen mode callback. public static bool fullScreenModeChange( int eventType, EmscriptenFullScreenChangeEvent e, Object userObject ) { /* Stream.writeLine( "fullScreenModeChange - called" ); Stream.writeLine( "eventType = " + Integer.toString( eventType ) ); Stream.writeLine( "isFullscreen = " + Boolean.toString( e.isFullScreen ) ); Stream.writeLine( "fullscreenEnabled = " + Boolean.toString( e.fullScreenEnabled ) ); Stream.writeLine( "nodeName = " + e.nodeName ); Stream.writeLine( "id = " + e.id ); Stream.writeLine( "userObject = " + (String)userObject );*/ // Abort if in emscripten full-window mode. if ( displayMode == DisplayMode.FullWindow ) { return true; } // Check for full screen mode. if ( e.isFullScreen ) { // Update the size of the window. sizeX = e.elementWidth; sizeY = e.elementHeight; } else { // Update the size of the window. sizeX = initialSizeX; sizeY = initialSizeY; } // Set the canvas size. Emscripten.setCanvasSize( sizeX, sizeY ); // Set the window size. SDL2.SDL_SetWindowSize( w, sizeX, sizeY ); // Reshape the gears. gearsReshape( ); // Update the display mode state. if ( e.isFullScreen ) displayMode = DisplayMode.FullScreen; else displayMode = emscriptenActive ? DisplayMode.Canvas : DisplayMode.FullWindow; // Return 'true' for handling the event. return true; } // Resize event callback. public static bool resizeEventCallback( int eventType, EmscriptenUiEvent uiEvent, Object userObject ) { // Diagnostic code. /* Stream.writeLine( "resizeEventCallback - called" ); Stream.writeLine( "eventType = " + Integer.toString( eventType ) ); Stream.writeLine( "uiEvent.detail = " + Integer.toString( uiEvent.detail ) ); Stream.writeLine( "uiEvent.documentBodyClientWidth = " + Integer.toString( uiEvent.documentBodyClientWidth ) ); Stream.writeLine( "uiEvent.documentBodyClientHeight = " + Integer.toString( uiEvent.documentBodyClientHeight ) ); Stream.writeLine( "uiEvent.windowInnerWidth = " + Integer.toString( uiEvent.windowInnerWidth ) ); Stream.writeLine( "uiEvent.windowInnerHeight = " + Integer.toString( uiEvent.windowInnerHeight ) ); Stream.writeLine( "uiEvent.windowOuterWidth = " + Integer.toString( uiEvent.windowOuterWidth ) ); Stream.writeLine( "uiEvent.windowOuterHeight = " + Integer.toString( uiEvent.windowOuterHeight ) ); Stream.writeLine( "uiEvent.scrollTop = " + Integer.toString( uiEvent.scrollTop ) ); Stream.writeLine( "uiEvent.scrollLeft = " + Integer.toString( uiEvent.scrollLeft ) ); Stream.writeLine( "userObject = " + (String)userObject );*/ // Check for full-window mode. if ( displayMode == DisplayMode.FullWindow ) { // Get the new size. sizeX = uiEvent.windowInnerWidth; sizeY = uiEvent.windowInnerHeight; // Update the canvas size. Emscripten.setCanvasSize( sizeX, sizeY ); // Update the SDL window size. SDL2.SDL_SetWindowSize( w, sizeX, sizeY ); // Reshape the gears. gearsReshape( ); } // Return 'true' for handling the event. return true; } public static void renderFrame( ) { SDL_Event e = SDL2.SDL_PollEvent( ); if ( e != null ) { switch ( e.id ) { case SDL2.SDL_WINDOWEVENT : { onWindowEvent( (SDL_WindowEvent)e ); break; } case SDL2.SDL_MOUSEBUTTONDOWN : { onMouseButtonDown( (SDL_MouseButtonEvent)e ); break; } case SDL2.SDL_MOUSEBUTTONUP : { onMouseButtonUp( (SDL_MouseButtonEvent)e ); break; } case SDL2.SDL_MOUSEMOTION : { onMouseMotion( (SDL_MouseMotionEvent)e ); break; } case SDL2.SDL_MOUSEWHEEL : { onMouseWheel( (SDL_MouseWheelEvent)e ); break; } case SDL2.SDL_KEYDOWN : { // Cast to a keyboard event. SDL_KeyboardEvent kbEvent = (SDL_KeyboardEvent)e; // Switch on the event key. switch ( kbEvent.sym ) { case SDL2.SDLK_f : { // Stream.writeLine( "'f' key pressed." ); // Abort if in emscripten full window mode. if ( emscriptenActive && ( displayMode == DisplayMode.FullWindow ) ) { break; } // Toggle full screen mode. toggleFullScreenMode( ); break; } case SDL2.SDLK_r : { // Stream.writeLine( "'r' key pressed." ); // Reset the spin and velocity matrices. spinMatrix = MatrixMath.identity( ); velocityMatrix = MatrixMath.identity( ); // Reset the view rotation. view_rot = { 20.0f, 30.0f, 0.0f }; break; } case SDL2.SDLK_s : { // Stream.writeLine( "'s' key pressed." ); // Stop / restart the gears turning. stopGearsTurning = !stopGearsTurning; break; } case SDL2.SDLK_w : { // Stream.writeLine( "'w' key pressed." ); // Only allow if using emscripten and not in full screen mode. if ( emscriptenActive && ( displayMode != DisplayMode.FullScreen ) ) { // Toggle full window mode. toggleFullWindowMode( ); } break; } case SDL2.SDLK_SPACE : { // Stream.writeLine( "'SPACE' key pressed." ); // Pause / resume spinning. spinPaused = !spinPaused; break; } case SDL2.SDLK_ESCAPE : { // Stream.writeLine( "'ESC' key pressed." ); // Exit full screen mode for the desktop app. if ( !emscriptenActive && ( displayMode == DisplayMode.FullScreen ) ) toggleFullScreenMode( ); // Exit full window mode for the emscripten app. if ( emscriptenActive && ( displayMode == DisplayMode.FullWindow ) ) toggleFullWindowMode( ); break; } } break; } case SDL2.SDL_QUIT : { // Stream.writeLine( "SDL_QUIT" ); done = true; // When running in a desktop app. break; } } } // Manually getting the state of the keys is more frequent and smoother, than keyboard events. if ( keyboardState[ SDL2.SDL_SCANCODE_LEFT ] != 0 ) view_rot[ 1 ] += 3.0; if ( keyboardState[ SDL2.SDL_SCANCODE_RIGHT ] != 0 ) view_rot[ 1 ] -= 3.0; if ( keyboardState[ SDL2.SDL_SCANCODE_UP ] != 0 ) view_rot[ 0 ] += 3.0; if ( keyboardState[ SDL2.SDL_SCANCODE_DOWN ] != 0 ) view_rot[ 0 ] -= 3.0; if ( keyboardState[ SDL2.SDL_SCANCODE_Z ] != 0 ) { // Brackets are needed here for the bitwise AND operator - // - because the equality operator has a slightly higher precedence. if ( ( SDL2.SDL_GetModState( ) & SDL2.KMOD_SHIFT ) != 0 ) { view_rot[ 2 ] -= 3.0; } else { view_rot[ 2 ] += 3.0; } } // Keep the view rotation values within 0-360 range. for ( int i = 0; i < 3; ++i ) { if ( view_rot[ i ] > 360.0f ) view_rot[ i ] -= 360.0f; if ( view_rot[ i ] < 0.0f ) view_rot[ i ] += 360.0f; } gearsDraw( ); SDL2.SDL_GL_SwapWindow( w ); // Update the gears animation and output the FPS. gearsUpdate( ); } public static void onWindowEvent( SDL_WindowEvent e ) { // Stream.writeLine( "SDL_WINDOWEVENT" ); // Check for a non null reference after the object cast. if ( e != null ) { switch ( e.event ) { case SDL2.SDL_WINDOWEVENT_RESIZED : { // Stream.writeLine( "SDL_WINDOWEVENT_RESIZED" ); // Update the size of the window. sizeX = e.data1; sizeY = e.data2; // Recalculate the projection matrix. gearsReshape( ); break; } } } } public static void onMouseButtonDown( SDL_MouseButtonEvent e ) { // Stream.writeLine( "SDL_MOUSEBUTTONDOWN" ); // Check for a non null reference after the object cast. if ( e != null ) { // byte b = e.button; // Stream.writeLine( Byte.toString( b ) ); if ( e.button == SDL2.SDL_BUTTON_LEFT ) { // Stream.writeLine( "Left button pressed." ); // --- DOUBLE CLICK DETECTION --- if ( e.clicks == 2 ) { // First, check and exit full screem mode if in the emscripten app. if ( emscriptenActive && ( displayMode == DisplayMode.FullScreen ) ) { toggleFullScreenMode( ); // Exit full screen mode. return; // Exit, do no more. } if ( emscriptenActive ) { toggleFullWindowMode( ); } else { toggleFullScreenMode( ); } // Exit so we don't start a spin-drag on a double-click. return; } // Update the state of the left mouse button. mouseButtonDown = true; // Stop the cube from spinning. velocityMatrix = MatrixMath.identity( ); } } } public static void onMouseButtonUp( SDL_MouseButtonEvent e ) { // Stream.writeLine( "SDL_MOUSEBUTTONUP" ); // Check for a non null reference after the object cast. if ( e != null ) { // Byte b = new Byte( e.button ); // Stream.writeLine( b.toString( ) ); if ( e.button == SDL2.SDL_BUTTON_LEFT ) { // Stream.writeLine( "Left button released." ); // Update the state of the left mouse button. mouseButtonDown = false; } } } // Mouse motion callback method. public static void onMouseMotion( SDL_MouseMotionEvent e ) { // Stream.writeLine( "SDL_MOUSEMOTION" ); // Check for a non-null reference. if ( e != null ) { // Get the cursor co-ordinates. int x = e.x; int y = e.y; // Stream.writeLine( "x = " + Integer.toString( x ) + " y = " + Integer.toString( y ) ); // Check if the left mouse button is currently pressed. if ( ( e.state & SDL2.SDL_BUTTON_LMASK ) != 0 ) { // Stream.writeLine( "Left button pressed (mouse motion)." ); // SDL_Log( "Mouse Button 1 (left) is pressed." ); // Calculate the delta (change) since the last frame. // NOTE: We divide by two to slightly reduce the spin speed. double dx = (double)( x - mouseX ) / 2.0; double dy = (double)( y - mouseY ) / 2.0; // 1. Create a temporary rotation matrix for JUST this frame's movement. // 2. Multiply it by the existing spinMatrix. // By multiplying the NEW rotation on the LEFT, we rotate // around the "Screen Axes" rather than the "Object Axes". float[] frameRot = MatrixMath.identity( ); frameRot = MatrixMath.rotate( frameRot, (float)( Math.PI * dy / 180.0 ), 1, 0, 0 ); // Degrees to radians. frameRot = MatrixMath.rotate( frameRot, (float)( Math.PI * dx / 180.0 ), 0, 1, 0 ); // Degrees to radians. // This is the magic line: New Rotation * Old Rotation = Screen-relative movement. spinMatrix = MatrixMath.multiply( frameRot, spinMatrix ); // Store our current velocity. velocityMatrix = frameRot; mouseButtonDown = true; // Update the mouse button state. spinPaused = false; // Resume spinning. } // Record the cursor co-ordinates. mouseX = x; mouseY = y; } } // Mouse wheel callback method. public static void onMouseWheel( SDL_MouseWheelEvent e ) { // Check for a non-null reference. if ( e != null ) { // e.y is +1 for scroll up, -1 for scroll down. // Multiply by 10.0 for greater speed. view_rot[ 2 ] += (float)( e.y * 10.0 ); } } private static void gearsInit( ) { OpenGL.glEnable( OpenGL.GL_CULL_FACE ); OpenGL.glEnable( OpenGL.GL_DEPTH_TEST ); // Minimal debug shaders. /* String vertCode = "precision highp float; \n" + "attribute vec3 position; \n" + "uniform mat4 ModelViewProjectionMatrix; \n" + "void main() { \n" + " gl_Position = ModelViewProjectionMatrix * vec4( position, 1.0 ); \n" + "} \n"; String fragCode = "precision highp float; \n" + "void main( ) { \n" + " gl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 ); // Bright red. \n" + "} \n";*/ // The shaders. String vertCode = "#ifdef GL_ES \n" + "precision highp float; // Set high floating-point precision for mobile GPUs. \n" + "#endif \n" + " \n" + "attribute vec3 position; // Raw vertex position from the buffer. \n" + "attribute vec3 normal; // Surface normal vector for lighting calculations. \n" + " \n" + "uniform mat4 ModelViewProjectionMatrix; // Combined Perspective and View matrix. \n" + "uniform mat4 NormalMatrix; // Matrix to transform normals into the correct orientation. \n" + " \n" + "varying float v_diffuse; // Passes the calculated light intensity to the Fragment Shader. \n" + " \n" + "void main( ) \n" + "{ \n" + " // 1. Define the light source direction (pointing toward the screen). \n" + " vec3 lightDir = normalize( vec3( 5.0, 5.0, 10.0 ) ); \n" + " \n" + " // 2. Transform the normal into eye-space (using .xyz to ensure vec3 type safety). \n" + " vec3 transformedNormal = normalize( ( NormalMatrix * vec4( normal, 0.0 ) ).xyz ); \n" + " \n" + " // 3. Calculate diffuse light (dot product determines how directly the light hits the surface). \n" + " v_diffuse = max( dot( transformedNormal, lightDir ), 0.0 ); \n" + " \n" + " // 4. Project the vertex position into 2D screen space. \n" + " gl_Position = ModelViewProjectionMatrix * vec4( position, 1.0 ); \n" + "} \n"; String fragCode = "#ifdef GL_ES \n" + "precision highp float; // Precision must match the Vertex Shader. \n" + "#endif \n" + " \n" + "varying float v_diffuse; // The light intensity received from the Vertex Shader. \n" + "uniform vec4 MaterialColor; // The base color of the gear (Red, Green, or Blue). \n" + " \n" + "void main( ) \n" + "{ \n" + " // 1. Define a base 'ambient' light so the dark sides of the gears aren't pitch black. \n" + " vec3 ambient = vec3( 0.2, 0.2, 0.2 ); \n" + " \n" + " // 2. Combine the base color with the total light (ambient + diffuse). \n" + " // Then output the final pixel color with full opacity (1.0). \n" + " gl_FragColor = vec4( MaterialColor.rgb * ( ambient + v_diffuse ), 1.0 ); \n" + "} \n"; // Create and compile the vertex shader. vertShader = OpenGL.glCreateShader( OpenGL.GL_VERTEX_SHADER ); OpenGL.glShaderSource( vertShader, vertCode ); OpenGL.glCompileShader( vertShader ); // Check the compilation status. int vertShaderStatus = OpenGL.glGetShaderiv( vertShader, OpenGL.GL_COMPILE_STATUS ); Stream.writeLine( "vertShaderStatus: " + Integer.toString( vertShaderStatus ) ); int vertShaderInfoLogLength = OpenGL.glGetShaderiv( vertShader, OpenGL.GL_INFO_LOG_LENGTH ); Stream.writeLine( "vertShaderInfoLogLength: " + Integer.toString( vertShaderInfoLogLength ) ); String vertShaderLog = OpenGL.glGetShaderInfoLog( vertShader ); Stream.writeLine( "vertShaderLog:" ); Stream.writeLine( vertShaderLog ); // Create and compile the fragment shader. fragShader = OpenGL.glCreateShader( OpenGL.GL_FRAGMENT_SHADER ); OpenGL.glShaderSource( fragShader, fragCode ); OpenGL.glCompileShader( fragShader ); // Check the compilation status. int fragShaderStatus = OpenGL.glGetShaderiv( fragShader, OpenGL.GL_COMPILE_STATUS ); Stream.writeLine( "fragShaderStatus: " + Integer.toString( fragShaderStatus ) ); int fragShaderInfoLogLength = OpenGL.glGetShaderiv( fragShader, OpenGL.GL_INFO_LOG_LENGTH ); Stream.writeLine( "fragShaderInfoLogLength: " + Integer.toString( fragShaderInfoLogLength ) ); String fragShaderLog = OpenGL.glGetShaderInfoLog( fragShader ); Stream.writeLine( "fragShaderLog:" ); Stream.writeLine( fragShaderLog ); // Create the shader program. shaderProgram = OpenGL.glCreateProgram( ); OpenGL.glAttachShader( shaderProgram, vertShader ); OpenGL.glAttachShader( shaderProgram, fragShader ); // Bind the attribute locations *before* we link the shader prgram. // NOTE: These calls are very important for Android OS, otherwise we have just a blank screen. OpenGL.glBindAttribLocation( shaderProgram, 0, "position" ); OpenGL.glBindAttribLocation( shaderProgram, 1, "normal" ); // Link the shader program. OpenGL.glLinkProgram( shaderProgram ); // Check the shader program link status. int shaderProgramLinkStatus = OpenGL.glGetProgramiv( shaderProgram, OpenGL.GL_LINK_STATUS ); Stream.writeLine( "shaderProgramStatus: " + Integer.toString( shaderProgramLinkStatus ) ); int shaderProgramInfoLogLength = OpenGL.glGetProgramiv( shaderProgram, OpenGL.GL_INFO_LOG_LENGTH ); Stream.writeLine( "shaderProgramInfoLogLength: " + Integer.toString( shaderProgramInfoLogLength ) ); String shaderProgramInfoLog = OpenGL.glGetProgramInfoLog( shaderProgram ); Stream.writeLine( "shaderProgramInfoLog:" ); Stream.writeLine( shaderProgramInfoLog ); // Use the shader program. OpenGL.glUseProgram( shaderProgram ); // Get the locations of the uniforms so we can access them. modelViewProjectionMatrix_location = OpenGL.glGetUniformLocation( shaderProgram, "ModelViewProjectionMatrix" ); normalMatrix_location = OpenGL.glGetUniformLocation( shaderProgram, "NormalMatrix" ); lightSourcePosition_location = OpenGL.glGetUniformLocation( shaderProgram, "LightSourcePosition" ); materialColor_location = OpenGL.glGetUniformLocation( shaderProgram, "MaterialColor" ); // The direction of the directional light for the scene. float[] lightSourcePosition = { 5.0, 5.0, 10.0, 1.0 }; // Set the view rotation. view_rot = { 20.0f, 30.0f, 0.0f }; // Set the angle. angle = 0.0f; // Set the LightSourcePosition uniform which is constant throught the program. OpenGL.glUniform4fv( lightSourcePosition_location, 1, lightSourcePosition ); // Initialize the spin matrices. spinMatrix = MatrixMath.identity( ); velocityMatrix = MatrixMath.identity( ); // Create the gears. gear1 = new Gear( 1.0f, 4.0f, 1.0f, 20, 0.7f ); gear2 = new Gear( 0.5f, 2.0f, 2.0f, 10, 0.7f ); gear3 = new Gear( 1.3f, 2.0f, 0.5f, 10, 0.7f ); } private static void gearsReshape( ) { setPerspective( (uint)sizeX, (uint)sizeY ); // Set the viewport. OpenGL.glViewport( 0, 0, sizeX, sizeY ); } private static void setPerspective( uint width, uint height ) { // Set the projection matrix. projectionMatrix = MatrixMath.perspective( 45.0, width / (float)height, 1.0f, 100.0f ); } private static void gearsDraw( ) { float[] red = { 0.8f, 0.1f, 0.0f, 1.0f }; float[] green = { 0.0f, 0.8f, 0.2f, 1.0f }; float[] blue = { 0.2f, 0.2f, 1.0f, 1.0f }; float[] transform = MatrixMath.identity( ); OpenGL.glClearColor( 0.5f, 0.5f, 0.5f, 0.0f ); OpenGL.glClear( OpenGL.GL_COLOR_BUFFER_BIT | OpenGL.GL_DEPTH_BUFFER_BIT ); // Translate and rotate the view. transform = MatrixMath.translate( transform, 0, 0, -20 ); // If the mouse button is not pressed and the spinning is not paused, keep applying the last velocity. if ( !mouseButtonDown && !spinPaused ) { // Apply the velocity to the orientation. spinMatrix = MatrixMath.multiply( velocityMatrix, spinMatrix ); } // Apply the spin orientation. transform = MatrixMath.multiply( transform, spinMatrix ); // Apply the gears view rotation. // NOTE: We also convert to radians here. transform = MatrixMath.rotate( transform, (float)( Math.PI * (double)view_rot[ 0 ] / 180.0 ), 1, 0, 0 ); transform = MatrixMath.rotate( transform, (float)( Math.PI * (double)view_rot[ 1 ] / 180.0 ), 0, 1, 0 ); transform = MatrixMath.rotate( transform, (float)( Math.PI * (double)view_rot[ 2 ] / 180.0 ), 0, 0, 1 ); // Draw the gears. gear1.drawGear( transform, -3.0f, -2.0f, angle, red, projectionMatrix, modelViewProjectionMatrix_location, normalMatrix_location, materialColor_location ); gear2.drawGear( transform, 3.1f, -2.0f, -2 * angle - 9.0f, green, projectionMatrix, modelViewProjectionMatrix_location, normalMatrix_location, materialColor_location ); gear3.drawGear( transform, -3.1f, 4.2f, -2 * angle - 25.0f, blue, projectionMatrix, modelViewProjectionMatrix_location, normalMatrix_location, materialColor_location ); } private static void gearsUpdate( ) { // Get the time elapsed via SDL. double dt, t = SDL2.SDL_GetTicks( ) / 1000.0; if ( tRot0 < 0.0 ) { tRot0 = t; } dt = t - tRot0; tRot0 = t; // Check the stopped flag. if ( !stopGearsTurning ) { // advance rotation for next frame. angle += (float)( 70.0 * dt ); // 70 degrees per second. } // Keep the angle in 0-360 degree range. if ( angle > 360.0f ) { angle -= 360.0f; } ++frames; if ( tRate0 < 0.0 ) { tRate0 = t; } // Every five seconds. if ( t - tRate0 >= 5.0 ) { float seconds = (float)( t - tRate0 ); float fps = (float)( frames / seconds ); Stream.writeLine( Integer.toString( frames ) + " frames in " + Float.toString( seconds ) + " seconds = " + Float.toString( fps ) + " FPS" ); tRate0 = t; frames = 0; } } // Toggle full screen mode. public static void toggleFullScreenMode( ) { // Check for emscripten mode. if ( emscriptenActive ) { // Check the full screen mode flag. if ( displayMode == DisplayMode.FullScreen ) { // Exit full screen mode. Emscripten.exitFullScreenMode( ); } else { // Enter full screen mode. Emscripten.enterFullScreenMode( ); } } else // Nova SDL2 desktop app. { // Get the current mode. // NOTE: Using a local variable. bool fullScreenMode = ( SDL2.SDL_GetWindowFlags( w ) & SDL2.SDL_WINDOW_FULLSCREEN ) != 0; // Are we in windowed mode? if ( !fullScreenMode ) { // Record the size of the window before switching to full screen mode. prevWindowSize = SDL2.SDL_GetWindowSize( w ); } // Toggle the full screen mode flag. fullScreenMode = !fullScreenMode; // Update the SDL2 full screen mode. SDL2.SDL_SetWindowFullscreen( w, fullScreenMode? SDL2.SDL_WINDOW_FULLSCREEN : 0 ); // Show the cursor. // SDL2.SDL_ShowCursor( (int)SDL2.SDL_ENABLE ); // Check for full screen mode. if ( fullScreenMode ) { // Get the display index. int displayIndex = SDL2.SDL_GetWindowDisplayIndex( w ); // Get the size of the whole desktop (the display resolution). SDL_DisplayMode dm = SDL2.SDL_GetDesktopDisplayMode( displayIndex ); // Check for a null reference. if ( dm == null ) { // Output an error message. Stream.write( "Error getting desktop display mode\n" ); // Abort the operation. return; } // Set the size to the current display. sizeX = dm.w; sizeY = dm.h; } else { // Revert to the previous window size. sizeX = prevWindowSize.w; sizeY = prevWindowSize.h; } // Set the window size. SDL2.SDL_SetWindowSize( w, sizeX, sizeY ); gearsReshape( ); // Update the display mode state. if ( fullScreenMode ) displayMode = DisplayMode.FullScreen; else displayMode = emscriptenActive ? DisplayMode.Canvas : DisplayMode.FullWindow; } } // Toggle full window mode. public static void toggleFullWindowMode( ) { // Check for full window mode. if ( displayMode == DisplayMode.FullWindow ) { // Restore the previous canvas size. sizeX = prevSizeX; sizeY = prevSizeY; SDL2.SDL_SetWindowSize( w, sizeX, sizeY ); gearsReshape( ); // Exit full window mode. Emscripten.exitFullWindowMode( ); // Update the state. displayMode = DisplayMode.Canvas; } else { // Backup the previous canvas size. prevSizeX = sizeX; prevSizeY = sizeY; sizeX = Emscripten.getClientWidth( ); sizeY = Emscripten.getClientHeight( ); // Enter full window mode. Emscripten.enterFullWindowMode( ); SDL2.SDL_SetWindowSize( w, sizeX, sizeY ); gearsReshape( ); // Update the state. displayMode = DisplayMode.FullWindow; } } } class Gear { // Gear constants. private int stripsPerTooth; private int verticesPerTooth; private int gearVertexStride; // The array of vertices comprising the gear. private float[][] vertices; // The number of vertices comprising the gear. private int nVertices; // The array of triangle strips comprising the gear. private VertexStrip[] strips; // The number of triangle strips comprising the gear. private int nstrips; // The Vertex Buffer Object holding the vertices in the graphics card. private uint[] vbos; public Gear( float inner_radius, float outer_radius, float width, int teeth, float tooth_depth ) { // Set the gear constants. stripsPerTooth = 7; verticesPerTooth = 34; gearVertexStride = 6; double[] s, c; // Sin & Cos arrays for various needed angles. float[] normal; // Calculate the radii used in the gear. float r0 = inner_radius; float r1 = outer_radius - tooth_depth / 2.0f; float r2 = outer_radius + tooth_depth / 2.0f; // Allocate memory for the triangle strip information. nstrips = stripsPerTooth * teeth; strips = new VertexStrip[ nstrips ]; for ( int i = 0; i < nstrips; ++i ) strips[ i ] = new VertexStrip( ); // Allocate memory for the vertices. vertices = new float[ verticesPerTooth * teeth ][ gearVertexStride ]; int cur_strip = 0; // The current strip. int v_i = 0; // The index of the current vertex. double da = 2.0 * Math.PI / teeth / 4.0; // Iterate through the teeth. for ( int i = 0; i < teeth; ++i ) { // Calculate needed sin/cos for varius angles. s = new double[ 5 ]; c = new double[ 5 ]; initSinCos( i, teeth, da, s, c ); // Create the 7 points (only x,y coords) used to draw a tooth. GearPoint[] p = { new GearPoint( r2, 1, c, s ), // 0 new GearPoint( r2, 2, c, s ), // 1 new GearPoint( r1, 0, c, s ), // 2 new GearPoint( r1, 3, c, s ), // 3 new GearPoint( r0, 0, c, s ), // 4 new GearPoint( r1, 4, c, s ), // 5 new GearPoint( r0, 4, c, s ) // 6 }; // Front face. startStrip( cur_strip, v_i ); normal = { 0, 0, 1.0 }; setGearVetex( v_i++, p, 0, +1, width, normal ); setGearVetex( v_i++, p, 1, +1, width, normal ); setGearVetex( v_i++, p, 2, +1, width, normal ); setGearVetex( v_i++, p, 3, +1, width, normal ); setGearVetex( v_i++, p, 4, +1, width, normal ); setGearVetex( v_i++, p, 5, +1, width, normal ); setGearVetex( v_i++, p, 6, +1, width, normal ); cur_strip = endStrip( cur_strip, v_i ); // Inner face. startStrip( cur_strip, v_i ); v_i = quadWithNormal( 4, 6, v_i, p, width, normal ); cur_strip = endStrip( cur_strip, v_i ); // Back face. startStrip( cur_strip, v_i ); normal = { 0, 0, -1.0 }; setGearVetex( v_i++, p, 6, -1, width, normal ); setGearVetex( v_i++, p, 5, -1, width, normal ); setGearVetex( v_i++, p, 4, -1, width, normal ); setGearVetex( v_i++, p, 3, -1, width, normal ); setGearVetex( v_i++, p, 2, -1, width, normal ); setGearVetex( v_i++, p, 1, -1, width, normal ); setGearVetex( v_i++, p, 0, -1, width, normal ); cur_strip = endStrip( cur_strip, v_i ); // Outer face. startStrip( cur_strip, v_i ); v_i = quadWithNormal( 0, 2, v_i, p, width, normal ); cur_strip = endStrip( cur_strip, v_i ); startStrip( cur_strip, v_i ); v_i = quadWithNormal( 1, 0, v_i, p, width, normal ); cur_strip = endStrip( cur_strip, v_i ); startStrip( cur_strip, v_i ); v_i = quadWithNormal( 3, 1, v_i, p, width, normal ); cur_strip = endStrip( cur_strip, v_i ); startStrip( cur_strip, v_i ); v_i = quadWithNormal( 5, 3, v_i, p, width, normal ); cur_strip = endStrip( cur_strip, v_i ); } // Set the number of vertices. nVertices = v_i; // Store the vertices in a vertex buffer object (VBO). vbos = new uint[ 1 ]; OpenGL.glGenBuffers( 1, vbos ); OpenGL.glBindBuffer( OpenGL.GL_ARRAY_BUFFER, vbos[ 0 ] ); float[] verticesFlat = createFlatArray( vertices ); OpenGL.glBufferData( OpenGL.GL_ARRAY_BUFFER, verticesFlat, OpenGL.GL_STATIC_DRAW ); } private float[] createFlatArray( float[][] vertices ) { int verticesSize = (int)vertices.size( ); float[] verticesFlat = new float[ verticesSize * gearVertexStride ]; for ( int i = 0; i < verticesSize; ++i ) { for ( int j = 0; j < gearVertexStride; ++j ) { verticesFlat[ ( i * gearVertexStride ) + j ] = vertices[ i ][ j ]; } } return verticesFlat; } private void initSinCos( int i, int teeth, double da, double[] s, double[] c ) { for ( uint j = 0; j < 5; ++j ) { double angle = i * 2.0 * Math.PI / teeth + da * j; s[ j ] = Math.sin( angle ); c[ j ] = Math.cos( angle ); } } private void startStrip( int cur_strip, int v_i ) { // Set the first vertex index for the current strip. strips[ cur_strip ].first = v_i; } // Fills a specified gear vertex. private void fillGearVertex( float[][] v, int i, float x, float y, float z, float[] n ) { v[ i ][ 0 ] = x; v[ i ][ 1 ] = y; v[ i ][ 2 ] = z; v[ i ][ 3 ] = n[ 0 ]; v[ i ][ 4 ] = n[ 1 ]; v[ i ][ 5 ] = n[ 2 ]; } private void setGearVetex( int v_i, GearPoint[] p, int point, int sign, float width, float[] normal ) { fillGearVertex( vertices, v_i, p[ point ].x, p[ point ].y, sign * width * 0.5f, normal ); } // Return the updated value of the current strip. private int endStrip( int cur_strip, int v_i ) { // Set the vertex count for the current strip. strips[ cur_strip ].count = v_i - strips[ cur_strip ].first; // Increment to the next strip. return ++cur_strip; } private int quadWithNormal( int p1, int p2, int v_i, GearPoint[] p, float width, float[] normal ) { normal = { p[ p1 ].y - p[ p2 ].y, -( p[ p1 ].x - p[ p2 ].x ), 0 }; setGearVetex( v_i++, p, p1, -1, width, normal ); setGearVetex( v_i++, p, p1, 1, width, normal ); setGearVetex( v_i++, p, p2, -1, width, normal ); setGearVetex( v_i++, p, p2, 1, width, normal ); // Return the updated vertex index. return v_i; } public void drawGear( float[] transform, float x, float y, float angle, float[] color, float[] projectionMatrix, int modelViewProjectionMatrix_location, int normalMatrix_location, int materialColor_location ) { float[] model_view; float[] normal_matrix; float[] model_view_projection; // Translate and rotate the gear. model_view = MatrixMath.translate( transform, x, y, 0 ); model_view = MatrixMath.rotate( model_view, (float)( 2 * Math.PI * angle / 360.0 ), 0, 0, 1 ); // Create and set the ModelViewProjectionMatrix. model_view_projection = MatrixMath.multiply( projectionMatrix, model_view ); OpenGL.glUniformMatrix4fv( modelViewProjectionMatrix_location, 1, false, model_view_projection ); // Create and set the NormalMatrix. It's the inverse transpose of the ModelView matrix. normal_matrix = MatrixMath.invert( model_view ); normal_matrix = MatrixMath.transpose( normal_matrix ); OpenGL.glUniformMatrix4fv( normalMatrix_location, 1, false, normal_matrix ); // Set the gear color. OpenGL.glUniform4fv( materialColor_location, 1, color ); // Set the vertex buffer object to use. OpenGL.glBindBuffer( OpenGL.GL_ARRAY_BUFFER, vbos[ 0 ] ); // Set up the position of the attributes in the vertex buffer object. OpenGL.glVertexAttribPointer( 0, 3, OpenGL.GL_FLOAT, false, 6 * 4, 0 ); OpenGL.glVertexAttribPointer( 1, 3, OpenGL.GL_FLOAT, false, 6 * 4, 3 * 4 ); // Enable the attributes. OpenGL.glEnableVertexAttribArray( 0 ); OpenGL.glEnableVertexAttribArray( 1 ); // Draw the triangle strips that comprise the gear. for ( int n = 0; n < nstrips; ++n ) { OpenGL.glDrawArrays( OpenGL.GL_TRIANGLE_STRIP, strips[ n ].first, strips[ n ].count ); } // Disable the attributes. OpenGL.glDisableVertexAttribArray( 1 ); OpenGL.glDisableVertexAttribArray( 0 ); } public void deleteGear( ) { // Free the gear's VBO. OpenGL.glDeleteBuffers( 1, vbos ); } } // Class describing the vertices in triangle strip. class VertexStrip { // The first vertex in the strip. public int first; // The number of consecutive vertices in the strip after the first. public int count; } // Gear point class (only for OpenGL gears). class GearPoint { public float x; public float y; public GearPoint( double r, int da, double[] c, double[] s ) { x = (float)( r * c[ da ] ); y = (float)( r * s[ da ] ); } } class MatrixMath { // Creates an identity 4x4 matrix. public static float[] identity( ) { float[] m = { 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 }; return m; } /** * Calculate a perspective projection transformation. * * fovy : the field of view in the y direction. * aspect : the view aspect ratio. * zNear : the near clipping plane. * zFar : the far clipping plane. */ public static float[] perspective( float fovy, float aspect, float zNear, float zFar ) { float[] m = identity( ); double sine, cosine, cotangent, deltaZ; double radians = (double)fovy / 2 * Math.PI / 180; deltaZ = zFar - zNear; sine = Math.sin( radians ); cosine = Math.cos( radians ); if ( ( deltaZ == 0 ) || ( sine == 0 ) || ( aspect == 0 ) ) { // Return null for an error. return null; } cotangent = cosine / sine; m[ 0 ] = (float)( cotangent / aspect ); m[ 5 ] = (float)cotangent; m[ 10 ] = (float)( -( (double)zFar + (double)zNear ) / deltaZ ); m[ 11 ] = -1; m[ 14 ] = (float)( -2 * (double)zNear * (double)zFar / deltaZ ); m[ 15 ] = 0; return m; } // Translates a 4x4 matrix. public static float[] translate( float[] m, float x, float y, float z ) { // Create the translation matrix. float[] t = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1 }; // Multiply and return the result. return multiply( m, t ); } // Rotates a 4x4 matrix. // NOTE: The angle is expected to be in radians. public static float[] rotate( float[] m, double angle, double x, double y, double z ) { double s = Math.sin( angle ); double c = Math.cos( angle ); // Create the rotation matrix. float[] r = { (float)( x * x * ( 1 - c ) + c ), (float)( y * x * ( 1 - c ) + z * s ), (float)( x * z * ( 1 - c ) - y * s ), 0, (float)( x * y * ( 1 - c ) - z * s ), (float)( y * y * ( 1 - c ) + c ), (float)( y * z * ( 1 - c ) + x * s ), 0, (float)( x * z * ( 1 - c ) + y * s ), (float)( y * z * ( 1 - c ) - x * s ), (float)( z * z * ( 1 - c ) + c ), 0, 0, 0, 0, 1 }; // Multiply and return the result. return multiply( m, r ); } // Inverts a 4x4 matrix. public static float[] invert( float[] m ) { float[] t = identity( ); // Extract and invert the translation part 't'. The inverse of a // translation matrix can be calculated by negating the translation // coordinates. t[ 12 ] = -m[ 12 ]; t[ 13 ] = -m[ 13 ]; t[ 14 ] = -m[ 14 ]; // Invert the rotation part 'r'. The inverse of a rotation matrix is // equal to its transpose. m[ 12 ] = m[ 13 ] = m[ 14 ] = 0; m = transpose( m ); // inv( m ) = inv( r ) * inv( t ) return multiply( m, t ); } // Transposes a 4x4 matrix. public static float[] transpose( float[] m ) { float[] t = { m[ 0 ], m[ 4 ], m[ 8 ], m[ 12 ], m[ 1 ], m[ 5 ], m[ 9 ], m[ 13 ], m[ 2 ], m[ 6 ], m[ 10 ], m[ 14 ], m[ 3 ], m[ 7 ], m[ 11 ], m[ 15 ] }; return t; } // Blocked matrix multiplication algorithm. public static float[] multiply( float[] m, float[] n ) { float[] tmp = new float[ 16 ]; for ( int i = 0; i < 16; ++i ) { tmp[ i ] = 0; int row = i / 4; int column = i % 4; for ( int j = 0; j < 4; ++j ) { tmp[ i ] += n[ row * 4 + j ] * m[ j * 4 + column ]; } } return tmp; } }