Computer Graphics with OpenGL

Table of Contents

1 Computer Graphics

1.1 Computer Graphics Overview [DRAFT]

Motivation

Outline of common computer graphics applications and a motivation for the study of this field.

Books

  • OpenGL Programming Guide: The Official Guide to Learning OpenG, Version 4.3
    • Note: Official Modern OpenGL documentation with lots of details and explanations.
  • Advanced Graphics Programming Using OpenGL - (The Morgan Kaufmann Series in Computer Graphics) 1st Edition
    • Brief: "This book brings the graphics programmer beyond the basics and introduces them to advanced knowledge that is hard to obtain outside of an intensive CG work environment. The book is about graphics techniques―those that don’t require esoteric hardware or custom graphics libraries―that are written in a comprehensive style and do useful things. It covers graphics that are not covered well in your old graphics textbook. But it also goes further, teaching you how to apply those techniques in real world applications, filling real world needs."
    • Note: Contains useful notes about data visualization and CAD implementations.
  • Interactive Computer Graphics: A Top-Down Approach With Shader-Based Opengl - 6th Edition
    • Brief: "This book is suitable for undergraduate students in computer science and engineering, for students in other disciplines who have good programming skills, and for professionals. Computer animation and graphics–once rare, complicated, and comparatively expensive–are now prevalent in everyday life from the computer screen to the movie screen. Interactive Computer Graphics: A Top-Down Approach with Shader-Based OpenGL, 6e, is the only introduction to computer graphics text for undergraduates that fully integrates OpenGL 3.1 and emphasizes application-based programming. Using C and C++, the top-down, programming-oriented approach allows for coverage of engaging 3D material early in the text so readers immediately begin to create their own 3D graphics. Low-level algorithms (for topics such as line drawing and filling polygons) are presented after readers learn to create graphics."
  • OpenGL Super Bible [ONLINE]

Videos - Related to Computer Graphics

Communities

1.2 GPU Accelerated Computer Graphics APIs

Native Graphics APIs (exposed as C-subroutines)

  • OpenGL (Khronos Group) - Main OpenGL specification
    • => Open standard, cross-platform and vendor-independent API for rendering 2D or 3D computer graphics with GPU (Graphics Processing Unit) acceleration. OpenGL can be used for implementing computer graphics, games, scientific vizualization, virtual reality and CADs - Computer Aided Design software. OpenGL API specificiation is maintained as an open-standard by the Krhonos Group industry consortium.
    • => OpenGL has two modes, immediate mode (a.k.a fixed-function pipeline, legacy-OpenGL) which is being depreacted, and retained mode (modern OpenGL) that delivers more performance and is based on buffer-objects and shaders.
    • => OpenGL Official Specification: Khronos OpenGL® Registry
  • OpenGL ES (Khronos Group)
    • => OpenGL for embedded systems, mobile devices and touch screen devices and so on. This API (Application Programming Interface) is widely used by many mobile games.
    • => Similar to OpenGL specfication, but supports only the retained-mode. OpenGL ES does not support immediate-mode. As a result, calls to legacy OpenGL subroutines such as glBegin(), glEnd(), glRotate(), glTranslate(), …, are not supported.
  • Vulkan_(Khronos Group)
    • => Graphics API with GPU acceleration that provides more low-level GPU control and less overhead than OpenGL. This API is designed for taking more advantag of multi-core CPU architectures and performing tasks in parallel.
  • DirectX / Direct3D (Microsoft inc.) - Windows-only
    • => Microsoft's graphics API for accessing the GPU hardware. It is only available on operating systems based Windows-NT kernel and Windows-CE kernel (embedded version of Windows-NT).
  • Metal (Apple inc.)
    • => Apple-only API for rendering 2D or 3D computer graphics with GPU acceleration. This API is available only on iOS and MacOSX operating systems. On iOS and MacOSX, Apple is deprecating and phasing out OpenGL in favor of its own API.

Web Computer Graphics APIs (exposed as Javascript/ECMAScript subroutines)

Further Reading

General:

GPU Listing:

Image Resolution DPI, PPI and so on:

1.3 OpenGL Documentation

OpenGL:

WebGL:

  • WebGL Overview - The Khronos Group Inc
    • "WebGL is a cross-platform, royalty-free web standard for a low-level 3D graphics API based on OpenGL ES, exposed to ECMAScript via the HTML5 Canvas element. Developers familiar with OpenGL ES 2.0 will recognize WebGL as a Shader-based API using GLSL, with constructs that are semantically similar to those of the underlying OpenGL ES API. It stays very close to the OpenGL ES specification, with some concessions made for what developers expect out of memory-managed languages such as JavaScript. WebGL 1.0 exposes the OpenGL ES 2.0 feature set; WebGL 2.0 exposes the OpenGL ES 3.0 API."
  • WebGL tutorial - Web APIs | MDN
  • Getting started with WebGL - Web APIs | MDN - WebGL Documentation.
  • WebGL Fundamentals

1.4 Terminology related to OpenGL and Computer Graphics

  • SIGGRAPH - international Association for Computing Machinery's Special Interest Group on Computer Graphics and Interactive Technique.
  • API - Application Programming Interface
  • Khronos Group - Industry consortium that takes care of OpenGL and Vulkan standards and specifications.
  • SGI - Silicon Graphics International - Company which developed first version of OpenGL. It was known for its SGI workstations.
  • OpenGL - Open Graphics Library
  • OpenGL Context
  • GLEW - OpenGL Extension Wrangler
  • ARB - OpenGL Architecture Review Board
  • OpenGL Immediate Mode (Fixed-Function Pipeline, Legacy OpenGL)
    • Also known as: Legacy OpenGL, Fixed-Function Pipeline
    • Drawing is mostly performed without storing data on GPU and by using subroutines calls to glScale(), glRotate(), glPush(), glPop(), glTranslate(), glBegin(), glEnd() and so on.
    • Immediate-mode is not supported by OpenGL ES or WebGL.
    • See: Fixed Function Pipeline - OpenGL Wiki
  • OpenGL retained mode (Programmable Pipeline, Modern OpenGL)
    • Also known as: Modern OpenGL, Programmable Pipeline
    • New and modern OpenGL API => Drawing is performed by storing data on the GPU via VBO (Vertex Buffer Objects) and by using shaders, programs that runs on GPU, for performing geometric vertex transformations, color and texturing computations.
    • More peformant than immediate-mode as the data is not sent to the GPU every frame.
  • GPU - Graphics Processing Unit
  • iGPU - Integrated GPU - The GPU is in the same processor chip silicon die. Most mobile GPUs are integrated. Most desktop computers also have dedicated GPUs built in the same chip as the CPU cores. This type of GPU is enough for most common tasks such as lightweight games, 2D games and watching youtube videos, but they are not suitable for heavy AAA (triple-A) games or for running CAD applications. Note: Most server-grade processor IC (Integrated Circuits), such as Intel Xeon lacks integrated GPU.
  • dGPU - Dedicated GPU - GPU available as a separated card and connected to the motherboard via PCI connection. They are suitable for triple-A games and GPGPU (General Purpose GPU Computing). The biggest manufacturers of those GPUs are Nvidia and AMD (Advanced Micro Devices).
  • eGPU - External GPU - GPU external to the motherboard and often connected via thunderbolt cable. External GPUs can be used on laptop machines, that often lack enough space for installing a dedicated GPU. Disadvantage: only newer motherboards have support for thunderbolt cables.
  • GPGPU - General Purpose Computing on GPU
    • APIs: OpenCL, Cuda, and so on.
    • Parallel non-graphics computations on GPU. GPGPU APIs take advantage of GPU parallel computing features for high performance computing.
  • AAA - Triple-A games.
  • DSA - Direct State Access
  • Vertex - 2D or 3D coordinates representing a point in the space.
  • DOF - Degrees Of Freedom
  • 2D - 2 dimensions (plane) / 2 coordinates (X, Y)
  • 3D - 3 dimensions (space) / 3 coordinates (X, Y, Z)
  • Homogenous Coordinate - Coordinate system using an extra dimension for encoding translation coordinate transformation in the same way as rotation matrices transformations.
    • 2D homogeneous coordinates: (X, Y, W = 1)
    • 3D homogeneous coordinates: (X, Y, Z, W = 1)
  • NDC - Normalized Device Coordinate
    • Default coordinates used by OpenGL (-1.0 to 1.0) for each axis. Any vertex that falls out of this range will not be visible on the screen.
  • MCS - Model Coordinate System
  • CTM - Current Transform Matrix
  • Buffer Object
  • VBO - Vertex Buffer Object
  • VAO - Vertex Array Object
  • FBO - Framebuffer Object
  • IBO - Index Buffer Object
  • UBO - Uniform Buffer Object
  • FPS - Frame Per Seconds
  • Shader - Program that runs on the GPU and performs vertex computations such as coordinate transformations (matrix multiplications), colors and texture computations.
  • GLSL - OpenGL shading programming language - for performing computer graphics calculations on the GPU hardware.
  • HLSL (High-Level Shader Language) - Microsft's DirectX shading language.
  • COP - Center Of Projection
  • CAD - Computer Aided Design
  • CAM - Computer Aided Manufacturing
  • CSG - Constructive Solid Geometry
  • Computer Graphics Data Structures
  • Common Mesh File Formats (Standardized file formats for mesh storage)
  • Article Related to Meshes
  • Resolution
    • PPI - Pixels Per Inch
    • DPI - Dots Per Inch
  • Display Types
    • CRT - Cathode Ray Tube
    • LCD - Liquid Crtystal Display
    • PDP - Plasma Display Panel
    • OLED

1.5 OpenGL companion libraries and 3D models

1.5.1 OpenGL Companion Libraries

OpenGL Loaders: [ESSENTIAL]

  • Libraries that abstracts OpenGL function pointers loading in a platform-independent way.
  • GLEW - OpenGL Extension Wrangler [MOST USED]
    • "The OpenGL Extension Wrangler Library (GLEW) is a cross-platform open-source C/C++ extension loading library. GLEW provides efficient run-time mechanisms for determining which OpenGL extensions are supported on the target platform. OpenGL core and extension functionality is exposed in a single header file. GLEW has been tested on a variety of operating systems, including Windows, Linux, Mac OS X, FreeBSD, Irix, and Solaris."
  • GLAD - [MOST-USED] Multi-Language GL/GLES/EGL/GLX/WGL Loader-Generator based on the official specs.
  • GitHub - cginternals/glbinding
    • "A C++ binding for the OpenGL API, generated using the gl.xml specification."
  • GitHub - anholt/libepoxy
    • "Epoxy is a library for handling OpenGL function pointer management for you."
  • GitHub - imakris/glatter
    • "An OpenGL loading library, with support for GL, GLES, EGL, GLX and WGL"
  • Galogen OpenGL Loader Generator
    • "Galogen is an OpenGL loader generator. Given an API version and a list of extensions, Galogen will produce corresponding headers and code that load the exact OpenGL entry points you need. The produced code can then be used directly by your C or C++ application, without having to link against any additional libraries."
  • GitHub - SFML/SFML-glLoadGen
    • Customized glLoadGen for SFML

Window System Abstraction

Libraries for window systems, event handling and OpenGL context abstraction: [ESSENTIAL]

  • Abstract platform-specific window system and event handling.
  • GLFW [BEST] [MOST-USED]
    • C library that provides graphics windows for OpenGL, Vulkan, OpenGL ES and deals with event handling.
  • SDL (Simple Direct Media Layer) [BEST] [MOST-USED]
    • Cross-platform C library that provides windows and event handling for many computer graphics APIs such as OpenGL, Vulkan and DirectX. SLD also has facilities for dealing with audio, joystick, CD-ROM, network and threads.
  • SFML (Simple and Fast Multimedia Library) [MOST-USED]
    • "SFML provides a simple interface to the various components of your PC, to ease the development of games and multimedia applications. It is composed of five modules: system, window, graphics, audio and network."
  • GLUT (FreeGlut) - OpenGL Utility Toolkit

Graphics Math Libraries

OpenGL Math and Computer Graphics Math: [ESSENTIAL]

  • GLM (OpenGL Mathematics Library) [MOST-USED]
    • Source code: https://github.com/g-truc/glm
    • Header-only C++ library that provides classes for computer graphics mathematics such as: 2D, 3D and homogeneous coordinate vector; 2D, 3D and homogeneous coordinate transformation matrices; quaternions and subroutines for computing camera, perspective or orthogonal transformation matrices.
  • GitHub - Kazade/kazmath - A C math library targeted at games
    • "Kazmath is a simple 3D maths library written in C. It was initially coded for use in my book, Beginning OpenGL Game Programming - Second edition, but rapidly gained a life of its own. Kazmath is now used by many different projects, and apparently is used in 25% of the worlds mobile games (yeah, I don't believe it either - but it's used in Cocos2d-x)."
  • GitHub - recp/cglm - Highly Optimized Graphics Math (glm) for C
  • See: GitHub - chunkyguy/Math-Library-Test - A comparison of the various major math libraries for speed and ease of use.

Image Loading for texture

  • GLI - OpenGL Image
    • "OpenGL Image (GLI) is a header only C++ image library for graphics software. GLI provides classes and functions to load image files (KTX and DDS), facilitate graphics APIs texture creation, compare textures, access texture texels, sample textures, convert textures, generate mipmaps, etc."
  • smoked-herring/sail - Squirrel Abstract Image Library
    • "SAIL is a format-agnostic cross-platform image decoding library providing rich APIs, from one-liners to complex use cases with custom I/O sources. It enables a client to read and write static, animated, multi-paged images along with their meta data and ICC profiles."
    • Note: Supports APNG, BMP, GIF, JPEG, PNG and TIFF image formats.
  • stb_image.h - single-file header-only C library for loading images from several file formats including, jpeg, bmp, tga, ppm, pgm, gif and so on.
  • SOIL - Simple OpenGL Image Library
    • "SOIL is a tiny C library used primarily for uploading textures into OpenGL. It is based on stb_image version 1.16, the public domain code from Sean Barrett (found here). It has been extended to load TGA and DDS files, and to perform common functions needed in loading OpenGL textures. SOIL can also be used to save and load images in a variety of formats."
  • freeimage
    • "FreeImage is an Open Source library project for developers who would like to support popular graphics image formats like PNG, BMP, JPEG, TIFF and others as needed by today's multimedia applications. FreeImage is easy to use, fast, multithreading safe, compatible with all 32-bit or 64-bit versions of Windows, and cross-platform (works both with Linux and Mac OS X)."
  • gldraw
    • "With glraw you can preconvert your texture assets and load them without the need of any image library. The generated raw files can easily be read. For this, glraw also provides a minimal Raw-File reader that you can either source-copy or integrate as C++ library into your project. Image to OpenGL texture conversion can be done either by glraws command line interface, e.g., within an existing tool-chain, or at run-time with glraw linked as asset library (requires linking Qt)."
  • lodepng
    • "LodePNG is a PNG image decoder and encoder, all in one, no dependency or linkage to zlib or libpng required. It's made for C (ISO C90), and has a C++ wrapper with a more convenient interface on top."
  • libpng
    • "libpng is the official PNG reference library. It supports almost all PNG features, is extensible, and has been extensively tested for over 23 years. The home site for development versions (i.e., may be buggy or subject to change or include experimental features) is https://libpng.sourceforge.io/, and the place to go for questions about the library is the png-mng-implement mailing list. libpng is available as ANSI C (C89) source code and requires zlib 1.0.4 or later (1.2.5 or later recommended for performance and security reasons). The current public release, libpng 1.6.37, fixes the use-after-free security vulnerability noted below, as well as an ARM NEON memory leak in the palette-to-RGB(A) expansion code (png_do_expand_palette())."
  • libspng
    • "libspng (simple png) is a C library for reading and writing Portable Network Graphics (PNG) format files with a focus on security and ease of use. It is licensed under the BSD 2-clause “Simplified” License."
  • IJG - Indepedent JPEG Group
    • "IJG is an informal group that writes and distributes a widely used free library for JPEG image compression. The first version was released on 7-Oct-1991. The current version is release 9d of 12-Jan-2020. This is a stable and solid foundation for many application's JPEG support. You can find our original code and some supporting documentation in the directory files. There is a Windows format package in zip archive format jpegsr9d.zip and a Unix format package in tar.gz archive format jpegsrc.v9d.tar.gz. A collection of modified versions with adaptions and error fixes for system maintenance is available on jpegclub.org in the directory support."

Asset/Object Loaders - Mesh Importing Libraries [ESSENTIAL]

Object loader / Asset import libraries:

  • Assimp - The Open-Asset-Import Library [MOST-USED]
    • "The Open Asset Import Library (short name: Assimp) is a portable Open-Source library to import various well-known 3D model formats in a uniform manner. The most recent version also knows how to export 3d files and is therefore suitable as a general-purpose 3D model converter. See the feature-list."
    • Note: Allows importing blender-generated models/assets in OpenGL.
    • Repository: https://github.com/assimp/assimp
    • See: https://www.khronos.org/opengl/wiki/Tools/Open_Asset_Import
  • OBJ-Loader
    • "OBJ Loader is a simple, header only, .obj model file loader that will take in a path to a file, load it into the Loader class object, then allow you to get the data from each mesh loaded. This will load each mesh within the model with the corresponding data such as vertices, indices, and material. Plus a large array of vertices, indices and materials which you can do whatever you want with."
  • tinyobjloader/tinyobjloader
    • "Tiny but powerful single file wavefront obj loader written in C++03. No dependency except for C++ STL. It can parse over 10M polygons with moderate memory and time. tinyobjloader is good for embedding .obj loader to your (global illumination) renderer ;-)."
  • codelibs/libdxfrw
    • "C++ library to read and write DXF/DWG (Autocad file format) files. - libdxfrw is a free C++ library to read and write DXF files in both formats, ascii and binary form. Also can read DWG files from R14 to the last V2015. It is licensed under the terms of the GNU General Public License version 2 (or at you option any later version)."

Text and Font Rendering

  • The FreeType Project
    • Brief: "It is written in C, designed to be small, efficient, highly customizable, and portable while capable of producing high-quality output (glyph images) of most vector and bitmap font formats."
  • OGLFT: OpenGL-FreeType Library
    • Brief: "This C++ library supplies an interface between the fonts on your system and an OpenGL or Mesa application. It uses the excellent FreeType library to read font faces from their files and renders text strings as OpenGL primitives."
  • GitHub - vallentin/glText - [HEADER-ONLY-LIBRARY]
    • Brief: "glText is a simple cross-platform single header text rendering library for OpenGL. glText requires no additional files (such as fonts or textures) for drawing text, everything comes pre-packed in the header."
  • GitHub - MartinPerry/OpenGL-Font-Rendering
    • Brief: "Rendering UNICODE fonts with OpenGL This library is still work-in-progress. This is a working beta version."
  • libdrawtext - OpenGL text rendering library
    • Brief: "Libdrawtext uses freetype2 for glyph rasterization. If you would rather avoid having freetype2 as a dependency, you can optionally compile libdrawtext without it, and use pre-rendered glyphmaps. Glyphmaps can be generated by the included font2glyphmap tool, or by calling dtx_save_glyphmap."
  • GitHub - codetiger/Font23D - Convert any text to a 3d mesh using any font style
    • Brief: "Font23D is a C++ library for creating a 3d mesh of any Text in the given True type font."

C++ Wrappers

  • OGplus - Self described as C++ Wrapper for modern OpengL.
    • Brief: "OGLplus is a header-only library which implements a thin object-oriented facade over the OpenGL® (version 3 and higher) C-language API. It provides wrappers which automate resource and object management and make the use of OpenGL in C++ safer and easier."
    • Repository: https://github.com/matus-chochlik/oglplu2
  • Oglwrap
    • Brief: "Oglwrap is a lightweight, cross-platform, object-oriented, header-only C++ wrapper for modern (2.1+) OpenGL, that focuses on preventing most of the trivial OpenGL errors, and giving as much debug information about the other errors, as possible."
  • Globjects - globjects is a cross-platform C++ wrapper for OpenGL API objects
    • Brief: "globjects provides object-oriented interfaces to the OpenGL API (3.0 and higher). It reduces the amount of OpenGL code required for rendering and facilitates coherent OpenGL use by means of an additional abstraction layer to glbinding and GLM. Common rendering tasks and processes are automated and missing features of specific OpenGL drivers are partially simulated or even emulated at run-time."

Form-based Graphical User Interface

  • Dear imgui
    • Brief: "Dear ImGui is a bloat-free graphical user interface library for C++. It outputs optimized vertex buffers that you can render anytime in your 3D-pipeline enabled application. It is fast, portable, renderer agnostic and self-contained (no external dependencies)."
    • See: An introduction to Dear Imgui Library

Non-categorized / Miscellaneous

  • bkaradzic/bgfx - "Bring Your Own Engine/Framework"
  • GLSDK - Unofficial OpenGL Software Development Kit
    • Brief: "The Unofficial OpenGL Software Development Kit is a collection of libraries and utilities that will help you get started working with OpenGL. It provides a unified, cross-platform build system to make compiling the disparate libraries easier. Many of the components of the SDK are C++ libraries. Each component of the SDK specifies the terms under which they are distributed. All licenses used by components are approximately like the MIT license in permissivity. The parts of the SDK responsible for maintaining the build, as well as all examples, are distributed under the MIT License."
  • NXPmicro/gtec-demo-framework
    • "A multi-platform framework for fast and easy demo development. The framework abstracts away all the boilerplate & OS specific code of allocating windows, creating the context, texture loading, shader compilation, render loop, animation ticks, benchmarking graph overlays etc. Thereby allowing the demo/benchmark developer to focus on writing the actual 'demo' code. Therefore demos can be developed on PC or Android where the tool chain and debug facilities often allows for faster turnaround time and then compiled and deployed without code changes for other supported platforms. The framework also allows for ‘real’ comparative benchmarks between the different OS and windowing systems, since the exact same demo/benchmark code run on all of them."
    • Supported Operating Systems: Android NDK; Linux with various windowing systems (Yocto); Ubuntu 18.04; Windows 7+

1.5.2 WebGL Companion Libraries

WebGL Companion Libraries: (OpenGL on the WEB)

  • glMatrix - GLM math library ported to Javascript. Computer graphics math library, similar to OpenGL GLM math library.
  • Math.GL - "math.gl is JavaScript math library focused on geospatial and 3D use cases, designed as a composable, modular toolbox. math.gl provides a core module with classic vector and matrix classes, and a suite of optional modules implementing various aspects of geospatial and 3D math. While the math.gl is highly optimized for use with the WebGL and WebGPU APIs, math.gl itself has no WebGL dependencies."
  • GLM-JS - "glm-js is an experimental JavaScript implementation of the OpenGL Mathematics (GLM) C++ Library."
  • Three.JS - High level wrapper library around WebGL, that provides many high level features that includes: camera objects; scene graphs; geometry; perspective; forward kinematics, inverse kinematics; graphics math library containing, matrices, vectors, quaternions; image loaders; animation and more.
  • phoria.js - [NOT WEBGL] "JavaScript library for simple 3D graphics and visualisation on a HTML5 canvas 2D renderer. It does not use WebGL. Works on all HTML5 browsers, including desktop, iOS and Android."
    • Note: It does use WebGL, but it is an interesting codebase about computer graphics algorithms implementation.

1.5.3 Sample 3D Models Repositories

Note: 3D models can be created using applications such as Blender, MeshlLab, Autodesk Maya, Solidworks and so on.

See also:

Obj File Documentation:

1.5.4 See also

1.6 Legacy/Obsolete OpenGL APIs / Subroutines

The following OpenGL subroutines are from the OpenGL immediate mode (fixed-function pipeline), which are obsolete and should be avoided as they incur on a significant overhead and they lack portability since they are not be available on OpenGL ES.

Note: The best way to check wether a OpenGL subroutine is obsolete is by searching for its name at http://docs.gl/. Subroutines without ES2, ES3 or GL4 hyperlinks are obsolete. For instance, by searching for 'glLight' at this web site, there are no ES2, ES3 or GL4 hyperlinks, which indicates that this subroutine is outdated.

Obsolete Subroutines:

  • Vertex:
    • => Modern OpenGL replacement: VBO (Vertex Buffer Object) and VAO (Vertex Array Object) and shader program.
    • glVertex2f() => 2D coordinate of current vertex
    • glVertex3f() => 3D Coordinate of current vertex
    • glNormal3f() => Sets the surface normal vector for the current vertex.
    • glColor3f() => Sets the color for the current vertex.
    • glColor4ub() => Sets the color for the current vertex using RGB in byte format from 0 to 255.
  • Begin/End:
    • glEnd()
    • glBegin()
  • Colors
    • glColor() => Modern OpenGL replacement: Fragement shader
    • glMaterial()
    • glVertexPointer()
  • Coordinate Transformation
    • Modern OpenGL Replacement: shader model matrix uniform variable which is set by the calling code. Since the matrix math (linear algebra) functionality is no longer provided by OpenGL, a third party math library is necessary.
    • glLoadIdentity() => Modern OpenGL replacement: glm::mat4(1.0);
    • glRotate() => Modern OpenGL replacement: glm::rotate();
    • glTranslate() => Modern OpenGL replacement: glm::translate();
    • glScale() => Modern OpenGL replacement: glm::scale();
    • glRotate3f()
    • glMatrixMode()
    • glFrustum() => Modern OpenGL replacement: glm::frustum();
    • gluLookAt() => Modern OpenGL replacement: glm::lookAt();
    • gluPerspective() => Modern OpenGL replacement: glm::perspective();
    • glMatrixMode()
    • glViewPort()
    • glOrtho()
    • glMultMatrix()
  • Camera affine transforms:
    • gluLookAt() => Replacement: glm::lookAt()
  • Save Context
    • glPop()
    • glPush()
  • Matrix Stack [DEPRECATED!]
    • Modern OpenGL replacement: OpenGL since 3.0, no longer provides a matrix stack. Now the calling code that has to keep track of transformation state.
    • glPushMatrix()
    • glPopMatrix()
  • Light and illumination:
    • Modern OpenGL Replacement: Light and illumination model are computed on fragment shader or vertex shader.
    • glLight()
    • glLightModel()
    • glMaterial()
    • glNormal3f()
  • Miscellaneous / Non Categorized
    • glEnableClientState()
    • glColorPointer()
    • glVertexPointer(*)
    • glLight*…
    • glMaterial*…
    • glDrawPixels()
    • glPixelZoom()
    • glRasterPos2i()

See:

1.7 Operating System Specific

1.7.1 Microsoft Windows NT

Command Line Shortcuts for Troubleshooting

The following applications can be accessed either from command line (cmd.exe shell) or via the shortcut Windows Key + R.

  • $ dxdiag => Tool for DirectX and OpenGL diagnostics.
  • $ devmgmt.msc => Device manager shortcut.
  • $ msinfo32 => View details about hardware.
  • $ systeminfo => View summarized information about operating system, hardware, RAM memory, disk space and patches. Note: It only works from command line.

GPU details and OpenGL Implementation Information

  • OpenGL Extensions Viewer 6 | realtech VR
    • Graphical application that allows viewing details about Vulkan and OpenGL implementation at the current machine.
    • Brief: "A reliable software which displays useful information about the current OpenGL 3D accelerator and new Vulkan 3D API. This program displays the vendor name, the version implemented, the renderer name and the extensions of the current OpenGL 3D accelerator."
  • GPU Caps View - Geeks3D
    • Brief: "A new version of GPU Caps Viewer is available. GPU Caps Viewer is a graphics card / GPU information and monitoring utility that quickly describes the essential capabilities of your GPU including GPU type, amount of VRAM , OpenGL, Vulkan, OpenCL and CUDA API support level."
  • GPU Test
    • Brief: "GpuTest is a cross-platform (Windows, Linux and Max OS X) GPU stress test and OpenGL benchmark. GpuTest comes with several GPU tests including some popular ones from Windows'world (FurMark or TessMark)."

Setting Default GPU on Windows

1.7.2 Linux-Based Operating Systems

Install OpenGL development dependencies

Install OpenGL development dependencies on Ubuntu or Debian-like distributions:

$ sudo apt-get install -y libgl1-mesa-dev libglu1-mesa-dev freeglut3-dev  libgl1-mesa-dev libxrandr-dev
$ sudo apt-get install -y libxinerama-dev libxcursor-dev libxi-dev

Information about OpenGL and graphics card

Vendor:

 $ >> glxinfo | grep -E "vendor"
server glx vendor string: SGI
client glx vendor string: Mesa Project and SGI
OpenGL vendor string: Intel

Rendering:

 $ >> glxinfo | grep -E "rendering"
direct rendering: Yes

Version:

 $ >> glxinfo | grep -E "version"
server glx version string: 1.4
client glx version string: 1.4
GLX version: 1.4
    Max core profile version: 4.6
    Max compat profile version: 4.6
    Max GLES1 profile version: 1.1
    Max GLES[23] profile version: 3.2
OpenGL core profile version string: 4.6 (Core Profile) Mesa 20.3.2
OpenGL core profile shading language version string: 4.60
OpenGL version string: 4.6 (Compatibility Profile) Mesa 20.3.2
OpenGL shading language version string: 4.60
OpenGL ES profile version string: OpenGL ES 3.2 Mesa 20.3.2
OpenGL ES profile shading language version string: OpenGL ES GLSL ES 3.20
    GL_EXT_shader_implicit_conversions, GL_EXT_shader_integer_mix,

Display:

 $ >> glxinfo | grep -E "display"
name of display: :1
display: :1  screen: 0

1.8 Computer Graphics Math

1.8.1 Overview

Computer graphics math is based on vector algebra, linear algebra and affine transforms. Those concepts are not exclusive to OpenGL, they are essential and universal to all computer graphics APIS - Application Programming Interfaces, including OpenGL, DirectX, Metal, Vulkan, WebGL and html5 canvas.

1.8.2 Vector Algebra

Given 2 3D vectors \(\vec{A} = [ x_A, y_A, z_A ]\) and \(\vec{B} = [ x_B, y_B, z_B ]\) , the following properties can be defined:

Vector Sum

\begin{equation} \vec{C} = \vec{A} + \vec{B} = (x_A + x_B) \hat{i} + (y_A + y_B) \hat{j} + (z_A + z_B) \hat{k} \end{equation}

Vector Difference / Subtraction

\begin{equation} \vec{C} = \vec{B} - \vec{A} = (x_B - x_A) \hat{i} + (y_B - y_A) \hat{j} + (z_B - z_A) \hat{k} \end{equation}

Vector Norm

The norm or magnitude of A, \(|| \vec{A} ||\) is given by:

\begin{equation} || \vec{A} || = \sqrt{ x_A^2 + y_A^2 + z_A^2 } \end{equation} \begin{equation} || \vec{A} ||^2 = \vec{A} \cdot \vec{A} \end{equation}

Normalized Vector / Unit Vector

A normalized vector, is a vector with the same direction than a given vector, but with norm equal to one. A normalized vector of A can be computed as:

\begin{equation} \text{normalized}( \vec{A} ) = \frac{ \vec{A} }{ || A || } = \frac{1}{ \sqrt{ x_A^2 + y_A^2 + z_A^2 } } . \vec{A} \end{equation} \begin{equation} \text{ normalized }( \vec{A} ) = \frac{1}{ \sqrt{ x_A^2 + y_A^2 + z_A^2 } } . ( x_A \cdot \hat{i} + y_A \cdot \hat{j} + z_A \cdot \hat{k} ) \end{equation}

Where:

  • \(\hat{i}\), \(\hat{j}\), \(\hat{k}\) are the unit vectors of axis X, Y and Z respectively.
\begin{equation} || \text{normalized}( \vec{A} ) || = 1 \end{equation}

Distance between two position (point) vectors

Given two vectors A and B which represent the position relative to the coordinate system origin or point. (0, 0, 0). The distance between A and B, or the length of the vector difference between A and B is determined by the following relation.

\begin{eqnarray*} \text{distance}( \vec{A}, \vec{B}) &=& || \vec{A} - \vec{B} || = || \vec{B} - \vec{A} || \\ \text{distance}( \vec{A}, \vec{B}) &=& \sqrt{ (x_A - x_B)^2 + (y_A - y_B)^2 + (z_A - z_B)^2 } \end{eqnarray*}

Element-wise Product

The element-wise product between two vectors is a vector which the components are the product between the two vectors components. As there is no standard mathematical notation for element-wise product between two vectors, the symbol \(\odot\) will be used for denoting this operation and avoiding confusion with dot product or vector product. This notation is useful for describing illumination (lighting) calculations, which most books and introduction materials present without a clear notation.

\begin{equation} \vec{A} \odot \vec{B} = \begin{bmatrix} x_A \cdot x_B \\ y_A \cdot y_B \\ z_A \cdot z_B \end{bmatrix} = \begin{bmatrix} x_A & 0 & 0 \\ 0 & y_A & 0 \\ 0 & 0 & z_A \end{bmatrix} \begin{bmatrix} x_B \\ y_B \\ z_B \end{bmatrix} \end{equation}

Dot Product (a.k.a - Scalar Product)

The vector dot product is:

\begin{equation} \text{dot}( \vec{A}, \vec{B} ) = \vec{A} \cdot \vec{B} = x_A \cdot x_B + y_A \cdot y_B + z_A \cdot z_B \end{equation}
  • Where:
    • \(|| \vec{A} ||\) is the norm of vector A
    • \(|| \vec{B} ||\) is the norm of vector B
  • The vectors \(\vec{A}\) and \(\vec{B}\) are orthogonal, the angle between them is 90 degrees, when the dot product is zero.

The dot product has the property:

\begin{equation} \text{dot}( \vec{A}, \vec{B} ) = \vec{A} \cdot \vec{B} = || \vec{A} || \cdot || \vec{B} || \cdot \cos \theta \end{equation}

And also:

\begin{equation} \cos \theta = \frac{ \vec{A} \cdot \vec{B} }{ || \vec{A} || \cdot || \vec{B} || } \end{equation}

Where:

  • \(\theta\) is the angle between vectors A and B.

If the vectors \(\vec{A}\) and \(\vec{B}\) are expressed as column matrices, the product can be computed as a matrix multiplication.

  • In the following equations. \(A^T\) is the transpose of matrix A.
\begin{equation} A = \begin{bmatrix} x_A \\ y_A \\ z_A \end{bmatrix} \end{equation} \begin{equation} B = \begin{bmatrix} x_B \\ y_B \\ z_B \end{bmatrix} \end{equation} \begin{equation} dot(\vec{A}, \vec{B}) = \vec{A} \cdot \vec{B} = A^T \cdot B = \begin{bmatrix} x_A & y_A & z_A \end{bmatrix} \cdot \begin{bmatrix} x_B \\ y_B \\ z_B \end{bmatrix} \end{equation}

Cross Product (a.k.a - Vector product)

The cross product between two vectors \(\vec{A}\) and \(\vec{B}\) results in a vector which is perpendicular (orthogonal) to both A and B.

\begin{equation} \text{cross}( \vec{A}, \vec{B} ) = \vec{A} \times \vec{B} = \begin{bmatrix} y_A \cdot z_B - z_A \cdot y_B \\ z_A \cdot x_B - x_A \cdot z_B \\ x_A \cdot y_B - y_A \cdot x_B \\ \end{bmatrix} = [A]_{\times} \cdot B \end{equation}

Where \([A]_{\times}\) is the cross product matrix.

\begin{equation} [A]_{\times} = \begin{bmatrix} 0 & -z_A & y_A \\ z_A & 0 & -x_A \\ -y_A & x_A & 0 \\ \end{bmatrix} \end{equation}

The cross product have the following properties:

\begin{equation} || \vec{A} \times \vec{B} || = || \vec{A} || \cdot || \vec{B} || \cdot \sin \theta \end{equation} \begin{equation} \vec{A} \times \vec{B} = || \vec{A} || \cdot || \vec{B} || \cdot \sin \theta \cdot \hat{n} \end{equation} \begin{equation} \vec{A} \times \vec{B} = - \vec{B} \times \vec{A} \end{equation}
  • Where:
    • \(\vec{n}\) is unit vector with the same direction as the cross product vector.
    • \(\theta\) is the angle between the two underlying vectors.

Vector Triple Product

\begin{equation} ( \vec{u} \times \vec{v} ) \times \vec{w} = ( \vec{v} \cdot \vec{w} ) \vec{u} + ( \vec{u} \cdot \vec{w}) \vec{v} = \text{dot}( \vec{v}, \vec{w} ) \vec{u} + \text{dot}( \vec{u}, \vec{w}) \vec{v} \end{equation}

Relation between vectors

Orthogonal (perpendicular) vectors:

Two vectors \(\vec{A}\) and \(\vec{B}\) are orthogonal, the angle between them is 90 degrees or PI/2 radians, if their dot product is zero.

\begin{equation} \vec{A} \cdot \vec{B} = 0 \end{equation}

Parallel vectors

Two vectors \(\vec{A}\) and \(\vec{B}\) are parallel when the angle between them are zero or 180 degrees. When this happens, then:

\begin{equation} \vec{A} \times \vec{B} = 0 \end{equation}

Same direction

Let the unit vector that points in the same direction of a vector \(\vec{A}\) be \([\vec{A}]_u\), where \([ \cdot ]_u\) is a operator that determines the unit vector of a vector.

\begin{equation} [\vec{A}]_u = \frac{1}{|| \vec{A} || } \vec{A} = \frac{1}{|| \vec{A} || } (x_A \cdot \hat{i} + y_A \cdot \hat{j} + z_A \cdot \hat{k} ) \end{equation}

A pair of vector \(\vec{A}\) and \(\vec{B}\) have the same direction both are parallel and the angle between them are zero. When this happens, both have the same unit vector or then angle between the are zero:

\begin{equation} [ \vec{A} ]_u = [ \vec{B} ]_u \end{equation}

Or:

\begin{equation} \vec{A} \cdot \vec{B} - || \vec{A} || \cdot || \vec{B} || = 0 \end{equation}

Two vectors are parallel and have opposite direction (angle between them is 180 degrees) if the following relation holds.

\begin{equation} \vec{A} \cdot \vec{B} + || \vec{A} || \cdot || \vec{B} || = 0 \end{equation}

Angle between two vectors

The angle between two 3D vectors can be determined by using their dot product and cross product.

\begin{equation} \theta = \text{atan2}( || \vec{A} \times \vec{B} ||, \vec{A} \cdot \vec{B} ) = \text{atan2} \left( \text{norm}( \text{cross}( \vec{A}, \vec{B})) , \text{dot}(\vec{A}, \vec{B}) \right) \end{equation}

1.8.3 Affine Transforms

Affine transforms, which are represented by matrices, are a particular class of linear transforms which preserves ratios between distances, colinearity and parallelism. Affine transforms has many applications that includes, computer graphics, computer vision, image processing, CAD (Computer Aided Design) and robotics.

Outline of properties preserved by affine transforms:

  • Ratios between distances.
  • Colinearity
    • => Points in the same line, remains in the same line, after the transform was applied.
  • Parallelism
    • => Lines that are parallels, remains parallel.

Types of geometric linear transforms:

  • Affine Transforms
    • => Preserves ratios, colinearity and parallelism. Some affine transforms are: identity, translation, rotation and scaling.
  • Projection Transforms
    • => They not preserve parallelism. However, they preserve colinearity. Affine transforms are a particular case of projection transforms.
    • => Some projection transforms are:
      • Orthogonal view transform
      • Projection view transform
  • Rigid body transforms
    • => Are a particular case of affine transforms. The set of rigid body transforms comprises: identity, translation and rotation. This set of transforms does not include shear and scaling.
    • => Use cases: Computer graphics, Newtonian mechanics, robotics, aerospace and many other cases.

Some affine transforms are:

  • Identity (Identity matrix) => Represents no transform, all points or vertices remain the same.
  • Translation
  • Scaling
  • Reflection
  • Rotation
  • Shear

Further Reading

General:

Coordinate Systems:

  • Coordinate system - Wikipedia
    • Brief: "n geometry, a coordinate system is a system that uses one or more numbers, or coordinates, to uniquely determine the position of the points or other geometric elements on a manifold such as Euclidean space."
  • Homogeneous coordinates - Wikipedia
    • Brief: "Homogeneous coordinates have a range of applications, including computer graphics and 3D computer vision, where they allow affine transformations and, in general, projective transformations to be easily represented by a matrix."

Rotation Matrix and right-hand-rule:

  • Right-hand rule - Wikipedia
    • Brief: "In mathematics and physics, the right-hand rule is a common mnemonic for understanding orientation of axes in three-dimensional space."
  • Rotation matrix - Wikipedia
    • Brief: "In linear algebra, a rotation matrix is a transformation matrix that is used to perform a rotation in Euclidean space."
  • Rotation formalisms in three dimensions - Wikipedia
    • Brief: "In geometry, various formalisms exist to express a rotation in three dimensions as a mathematical transformation. In physics, this concept is applied to classical mechanics where rotational (or angular) kinematics is the science of quantitative description of a purely rotational motion."
  • Axes conventions - Wikipedia
    • Brief: "In ballistics and flight dynamics, axes conventions are standardized ways of establishing the location and orientation of coordinate axes for use as a frame of reference."
  • Euler angles - Wikipedia
    • Brief: "The Euler angles are three angles introduced by Leonhard Euler to describe the orientation of a rigid body with respect to a fixed coordinate system."
  • Yaw, Pitch, Roll angles - Aircraft principal axes - Wikipedia
    • Brief: Rotation angles convention for aircrafts.
  • Celestial coordinate system - Wikipedia
    • Brief: "In astronomy, a celestial coordinate system (or celestial reference system) is a system for specifying positions of satellites, planets, stars, galaxies, and other celestial objects relative to physical reference points available to a situated observer (e.g. the true horizon and north cardinal direction to an observer situated on the Earth's surface)"
  • Conversion between quaternions and Euler angles - Wikipedia
    • Brief: "Spatial rotations in three dimensions can be parametrized using both Euler angles and unit quaternions. This article explains how to convert between the two representations. Actually this simple use of "quaternions" was first presented by Euler some seventy years earlier than Hamilton to solve the problem of magic squares."

Affine Tranform Matrices in SVG, Html5 Canvas and WebGL:

Affine Transform Matrices in DirectX (Direct3D):

Affine Transform Matrices in Java AWT:

Affine Transform Matrices in non-categorized APIs

1.8.4 2D Canonical Affine Transforms

General form of 2D affine transforms

2D affine transforms, including translation, rotation and scaling can be represented by matrices with the following format, that transforms homogeneous coordinates from one coordinate system to another. Homogeneous coordinates, are 2D or 3D coordinates with an extra pseudo-coordinate, often designated by 'w', for representing translations affine transforms in the same way as rotations and scaling.

\begin{equation} A = \begin{bmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

This affine transform performs the coordinate transformation from the coordinate frame C2 (which axis are x, y), which could an object local-space coordinate, to coordinate frame C1 (which axis are x', y') which could be a world-coordinate system. The matrix transforms homogenous coordinates, which are coordinates with an extra parameter w = 1 for allowing translation transformation to be expressed in the same way as rotation transformations.

\begin{equation} \begin{bmatrix} x' \\ y' \\ w' = 1 \end{bmatrix} = \begin{bmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ 0 & 0 & 1 \\ \end{bmatrix} \cdot \begin{bmatrix} x \\ y \\ w = 1 \end{bmatrix} = \begin{bmatrix} a_{11} \cdot x + a_{12} \cdot y + a_{13} \\ a_{21} \cdot x + a_{22} \cdot y + a_{23} \\ 1 \end{bmatrix} \end{equation}

The multiplication between two affine transforms also results in a affine transform. Consider two affine transforms \(A = a_{ij}\) and \(B = b_{ij}\). The product between these two affine transforms is also an affine transform.

\begin{equation} C = A \cdot B = \begin{bmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} b_{11} & b_{12} & b_{13} \\ b_{21} & b_{22} & b_{23} \\ 0 & 0 & 1 \\ \end{bmatrix} = \begin{bmatrix} c_{11} & c_{12} & c_{13} \\ c_{21} & c_{22} & c_{23} \\ 0 & 0 & 1 \end{bmatrix} \end{equation}

Where:

  • \(c_{11} = a_{11} \cdot b_{11} + a_{12} \cdot b_{21}\)
  • \(c_{12} = a_{11} \cdot b_{12} + a_{12} \cdot b_{22}\)
  • \(c_{21} = a_{21} \cdot b_{11} + a_{22} \cdot b_{21}\)
  • \(c_{22} = a_{21} \cdot b_{12} + a_{22} \cdot b_{22}\)
  • \(c_{13} = a_{11} \cdot b_{13} + a_{12} \cdot b_{23} + a_{13}\)
  • \(c_{23} = a_{21} \cdot b_{13} + a_{22} \cdot b_{23} + a_{23}\)

2D Canonical Affine Transforms

Identity matrix

  • Causes no coordinate transformation. The result coordinate system and vertices remain at the same position. The identity matrix is also a reasonable initial default value for the model matrix, view matrix and projection matrix.
\begin{equation} I = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation} \begin{equation} \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = I \cdot \begin{bmatrix} x \\ y \\ w = 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} \end{equation}

Translation:

  • Translate the coordinate system from one position into another and translate vertices.
\begin{equation} T = \begin{bmatrix} 1 & 0 & \Delta_x \\ 0 & 1 & \Delta_y \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation} \begin{equation} \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & \Delta_x \\ 0 & 1 & \Delta_y \\ 0 & 0 & 1 \\ \end{bmatrix} \cdot \begin{bmatrix} x \\ y \\ w = 1 \end{bmatrix} = \begin{bmatrix} x + \Delta_x \\ y + \Delta_y \\ 1 \end{bmatrix} \end{equation}

Scaling:

  • Increases or decreases object size. This transformation allows resizing an object without sending the vertices multiple times to the GPU in the case of a GPU accelerated graphics API.
  • Where: \(s_x\) is the scale for X axis and \(s_y\) is the scale for the y axis.
\begin{equation} S = \begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation} \begin{equation} \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \cdot \begin{bmatrix} x \\ y \\ w = 1 \end{bmatrix} = \begin{bmatrix} s_x \cdot x \\ s_y \cdot y \\ 1 \end{bmatrix} \end{equation}

Shear:

\begin{equation} H = \begin{bmatrix} 1 & h_x & 0 \\ h_y & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

Rotation around Z axis:

  • Where \(\theta\) is the angle of counterclockwise rotation around Z axis. A negative angle results in a rotation in the opposite direction.
\begin{equation} R = \begin{bmatrix} \cos \theta & -\sin \theta & 0 \\ \sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation} \begin{equation} \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} \cos \theta & -\sin \theta & 0 \\ \sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} x \cos \theta - y \sin \theta \\ x \sin \theta + y \cos \theta \\ 1 \end{bmatrix} \end{equation}

1.8.5 2D Window to Viewport transform

The window-to-viewport affine transform maps coordinates from a world-space window to a viewport (physical display coordinates). The world-space window defines what region of the world-space will be viewed. It is specified using the world-space coordinates \(x_{wmin}\), \(x_{wmax}\), \(y_{wmin}\) and \(y_{wmax}\). The viewport is a rectangle within the graphics display window to where the world-window coordinates will be mapped to. The viewport window is defined by the coordinates: \(x_{vmin}\), \(x_{vmax}\), \(y_{vmin}\), \(y_{vmax}\) - given in screen device-coordinates with origin at the graphics display bottom left corner.

Note: In OpenGL this transform can be obtained using the subroutines glViewport() and glm::ortho().

Applications

  • Draw in a limited area of screen. (Note: it is often possible to define multiple viewport windows.)
  • Draw using user-defined coordinates and make the drawing independent of the screen size.
  • Draw multiple views of the world-space.
  • Draw charts (a.k.a curve plotting).
  • Draw multiple charts on the same screen.

Parts:

  • World-Window (a.k.a clipping window) => Defines what the user wants to see from the world-space. Define by: \(x_{wmin}\), \(x_{wmax}\), \(y_{wmin}\) and \(y_{wmax}\).
    • \(x_{wmin}\) - Minimum X axis coordinate from window-space that can be viewed.
    • \(x_{wmax}\) - Maximum X axis coordinate from window-space that can be viewed.
    • \(y_{wmin}\) - Minimum y axis coordinate from window-space that can be viewed.
    • \(y_{wmax}\) - Maximum y axis coordinate from window-space that can be viewed.
  • Viewport => Defines where the user wants to see the world-window within the graphics display window (a.k.a canvas).
    • \(x_{vmin}\) => \(0 \leq x_{vmin} \leq w\)
    • \(x_{vmax}\) => \(0 \leq x_{vmin} \leq w\)
    • \(y_{vmin}\) => \(0 \leq y_{vmin} \leq h\)
    • \(y_{vmax}\) => \(0 \leq y_{vmax} \leq h\)
  • Default values of viewport:
    • \(x_{vmin} = 0\)
    • \(x_{vmax} = w\)
    • \(y_{vmin} = 0\)
    • \(y_{vmax} = h\)

Where:

  • h - graphics display screen height, often in pixels.
  • w - graphics display screen width, often in pixelss

world-window-to-viewport.png

Figure 1: World-to-viewport transform

The window-to-viewport transform matrix can be computed as:

\begin{equation} T_{ W \rightarrow V} = \begin{bmatrix} s_x & 0 & t_x \\ 0 & s_y & t_y \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

Where:

\begin{eqnarray*} s_x &=& \frac{\Delta x_{v} }{ \Delta x_{w} } = \frac{ x_{vmax} - x_{vmin} }{x_{wmax} - x_{wmin}} \\ s_y &=& \frac{\Delta y_{v} }{ \Delta y_{w} } = \frac{ y_{vmax} - y_{vmin} }{y_{wmax} - y_{wmin}} \\ t_x &=& x_{vmin} - s_x \cdot x_{wmin} \\ t_y &=& y_{vmin} - s_y \cdot y_{wmin} \\ \end{eqnarray*}

Coordinates from world-space can be mapped to the screen-device space by applying the affine transform \(T_{ W \rightarrow V}\) to the world-space coordinates, designated by \(x_w\), \(y_w\).

\begin{equation} \begin{bmatrix} x_v \\ y_v \\ 1 \end{bmatrix} = T_{ W \rightarrow V} \cdot \begin{bmatrix} x_w \\ y_w \\ 1 \end{bmatrix} \end{equation} \begin{equation} \begin{bmatrix} x_v \\ y_v \\ 1 \end{bmatrix} = \begin{bmatrix} s_x \cdot x_w + t_x \\ s_y \cdot y_w + t_y \\ 1 \end{bmatrix} = \begin{bmatrix} s_x (x_w - x_{wmin}) + x_{vmin} \\ s_y (x_y - y_{wmin}) + y_{vmin} \\ 1 \end{bmatrix} \end{equation}

Finally, the coordinates \(x_v\) and \(y_v\) can be determined in a more human-friendly way with the following expression:

\begin{eqnarray*} x_v &=& s_x (x_w - x_{wmin}) + x_{vmin} \\ y_v &=& s_y (y_w - y_{wmin}) + y_{vmin} \\ \end{eqnarray*}

Image distortion

The world-space propotions will only be preserved when the following predicate holds. For instance, a square in the world-space will look like a rectangle if the rations \(s_x\) and \(s_y\) are not equal.

\begin{equation} s_x = s_y \end{equation}

In orther words,

\begin{equation} \frac{ x_{vmax} - x_{vmin} }{x_{wmax} - x_{wmin}} = \frac{ y_{vmax} - y_{vmin} }{y_{wmax} - y_{wmin}} \end{equation}

Upper-left coordinate system

If the graphics API has a default upper-left coordinate system, where the origin is at the upper-left corner of the display window and Y axis is pointing downwards. The viewport matrix transform becomes:

\begin{equation} T_{ W \rightarrow VU} = \begin{bmatrix} s_x & 0 & t_x \\ 0 & -s_y & h - t_y \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

Then, the \(x_v\), \(y_v\) coordinates can be computed as:

\begin{eqnarray*} x_v &=& +s_x (x_w - x_{wmin}) + x_{vmin} \\ y_v &=& -s_y (y_w - y_{wmin}) + (h - y_{vmin}) \\ \end{eqnarray*}

References and further reading:

1.8.6 3D Affine Transforms

Notations for homogenous coordinate

Point-Vector - for denoting postion, location, point in space or vertex:

  • It represents a point or a vertex in space, a coordinate relative to the origin of the coordinate system. Some valid affinte transform operations are translation, rotation, shear and scaling.
\begin{equation} p = \begin{bmatrix} x \\ y \\ z \\ w = 1 \end{bmatrix} = \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} \end{equation}

Vector - for denoting force, acceleration, or difference between two point-vectors, and so on:

  • It represents entities, such as difference between two point-vectors, force, speed, angular speed, acceleration and so on, with a magnitude and direction. It does not make sense to apply translation and scaling to those entities, as a result the pseudo coordinate w is zero for this case.
\begin{equation} v = \begin{bmatrix} x \\ y \\ z \\ w = 0 \end{bmatrix} = \begin{bmatrix} x \\ y \\ z \\ 0 \end{bmatrix} \end{equation}

3D Affine Transforms

3D affine transforms can be represented by matrices with the following format.

\begin{equation} A = \begin{bmatrix} a_{11} & a_{12} & a_{13} & a_{14} \\ a_{21} & a_{22} & a_{23} & a_{24} \\ a_{31} & a_{32} & a_{33} & a_{34} \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

An affine transformation maps the coordinate system (X, Y, Z), which could be an object local coordinate system, to the coordinate system (X', Y', Z'), which could be a world coordinate system. In a similar manner to 2D homogeneous coordinates, an extra pseudo-coordinate w = 1 is added for expressing translations transformations as matrix multiplications, in the same way as rotations.

\begin{equation} \begin{bmatrix} x' \\ y' \\ z' \\ w' = 1 \end{bmatrix} = \begin{bmatrix} a_{11} & a_{12} & a_{13} & a_{14} \\ a_{21} & a_{22} & a_{23} & a_{24} \\ a_{31} & a_{32} & a_{33} & a_{34} \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \cdot \begin{bmatrix} x \\ y \\ z \\ w = 1 \end{bmatrix} \end{equation} \begin{equation} \begin{bmatrix} x' \\ y' \\ z' \\ 1 \end{bmatrix} = \begin{bmatrix} a_{11} \cdot x + a_{12} \cdot y + a_{13} \cdot z + a_{14} \\ a_{21} \cdot x + a_{22} \cdot y + a_{23} \cdot z + a_{24} \\ a_{31} \cdot x + a_{32} \cdot y + a_{33} \cdot z + a_{34} \\ 1 \end{bmatrix} \end{equation}

Combination between affine transforms

Consider two affine transforms \(A = a_{ij}\) and \(B = b_{ij}\). The product between those two transform is also an affine transform.

\begin{equation} C = A \cdot B = \left[\begin{matrix}a_{11} & a_{12} & a_{13} & a_{14}\\a_{21} & a_{22} & a_{23} & a_{24}\\a_{31} & a_{32} & a_{33} & a_{34}\\0 & 0 & 0 & 1\end{matrix}\right] \left[\begin{matrix}b_{11} & b_{12} & b_{13} & b_{14}\\b_{21} & b_{22} & b_{23} & b_{24}\\b_{31} & b_{32} & b_{33} & b_{34}\\0 & 0 & 0 & 1\end{matrix}\right] \end{equation} \begin{equation} C = \left[\begin{matrix}c_{11} & c_{12} & c_{13} & c_{14}\\c_{21} & c_{22} & c_{24} & c_{23}\\c_{31} & c_{32} & c_{33} & c_{34}\\0 & 0 & 0 & 1\end{matrix}\right] \end{equation}

Rotation affine transforms

Rotation affine transforms represents rotation around some axis or direction have the following form:

\begin{equation} R = \begin{bmatrix} r_{11} & r_{12} & r_{13} & 0 \\ r_{21} & r_{22} & r_{23} & 0 \\ r_{31} & r_{32} & r_{33} & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

Properties of Rotation Matrix Affine Transforms:

  • Rotation matrices are orthogonal matrices and have the following properties:
\begin{equation} R^T \cdot R = R \cdot R^T = I \end{equation} \begin{equation} det(R) = 1 \end{equation} \begin{equation} R^T = R^{-1} \end{equation}
  • Where:
    • R is a rotation matrix
    • \(R^T\) is the transpose of the rotation matrix R.
    • \(R^{-1}\) is the inverse of the rotation matrix R.

3D Canonical Affine Transforms

Translation:

  • Where: \(T^{-1}\) is the inverse transform.
\begin{equation} T = \begin{bmatrix} 1 & 0 & 0 & \Delta_x \\ 0 & 1 & 0 & \Delta_y \\ 0 & 0 & 1 & \Delta_z \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation} \begin{equation} T^{-1} = \begin{bmatrix} 1 & 0 & 0 & -\Delta_x \\ 0 & 1 & 0 & -\Delta_y \\ 0 & 0 & 1 & -\Delta_z \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

Translation transform applied to an homogeneous vector (x, y, z, w = 1):

\begin{equation} \begin{bmatrix} x' \\ y' \\ z' \\ w' = 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 & \Delta_x \\ 0 & 1 & 0 & \Delta_y \\ 0 & 0 & 1 & \Delta_z \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \cdot \begin{bmatrix} x \\ y \\ z \\ w = 1 \end{bmatrix} = \begin{bmatrix} x + \Delta_x \\ y + \Delta_y \\ z + \Delta_z \\ 1 \end{bmatrix} \end{equation}

Scaling:

  • Where: \(s_x\), \(s_y\), \(s_z\) are the scale factors for axis x, y, z and \(S^{-1}\) is the inverse transform (inverse matrix).
\begin{equation} S = \begin{bmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation} \begin{equation} S^{-1} = \begin{bmatrix} 1 / s_x & 0 & 0 & 0 \\ 0 & 1 / s_y & 0 & 0 \\ 0 & 0 & 1 / s_z & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

Scaling transform applied to a homogeneous coordinate vector:

\begin{equation} \begin{bmatrix} x' \\ y' \\ z' \\ w' = 1 \end{bmatrix} = S \cdot \begin{bmatrix} x \\ y \\ z \\ w = 1 \end{bmatrix} = \begin{bmatrix} s_x \cdot x \\ s_y \cdot y \\ s_z \cdot z \\ 1 \end{bmatrix} \end{equation}

Rotation around x axis

  • \(R_x^{-1} = R_x^T\) => The inverse is equal to the transpose.
\begin{equation} R_x(\alpha) = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos{\alpha} & -\sin{\alpha} & 0 \\ 0 & \sin{\alpha} & \cos{\alpha} & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

Transform \(R_x\) applied to a homogeneous vector (X, Y, Z, w = 1).

\begin{equation} \begin{bmatrix} x' \\ y' \\ z' \\ w' = 1 \end{bmatrix} = R_x(\alpha) \cdot \begin{bmatrix} x \\ y \\ z \\ w = 1 \end{bmatrix} = \begin{bmatrix} x \\ y \cdot \cos{\alpha} - z \cdot \sin{\alpha} \\ y \cdot \sin{\alpha} + z \cdot \cos{\alpha} \\ 1 \\ \end{bmatrix} \end{equation}

Rotation around y axis

  • \(R_y^{-1} = R_y^T\)
\begin{equation} R_y(\beta) = \begin{bmatrix} \cos{\beta} & 0 & \sin{\beta} & 0 \\ 0 & 1 & 0 & 0 \\ -\sin{\beta} & 0 & \cos{\beta} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \end{equation}

Transform \(R_y\) applied to a homogeneous vector:

\begin{equation} \begin{bmatrix} x' \\ y' \\ z' \\ w' = 1 \end{bmatrix} = R_y(\beta) \cdot \begin{bmatrix} x \\ y \\ z \\ w = 1 \end{bmatrix} = \begin{bmatrix} x \cdot \cos{\beta} + z \cdot \sin{\beta} \\ y \\ - x \cdot \sin{\beta} + z \cdot \cos{\beta} \\ \end{bmatrix} \end{equation}

Rotation around z axis

  • \(R_z^{-1} = R_z^T\)
\begin{equation} R_z(\theta) = \begin{bmatrix} \cos{\theta} & -\sin{\theta} & 0 & 0 \\ \sin{\theta} & \cos{\theta} & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

Transform \(R_z\) applied to a homogeneous vector:

\begin{equation} \begin{bmatrix} x' \\ y' \\ z' \\ w' = 1 \end{bmatrix} = R_z(\theta) \cdot \begin{bmatrix} x \\ y \\ z \\ w = 1 \end{bmatrix} = \begin{bmatrix} x \cdot \cos{\theta} - y \cdot \sin{\theta} \\ x \cdot \sin{\theta} + y \cdot \cos{\theta} \\ z \\ 1 \end{bmatrix} \end{equation}

Rotation around arbirtary axis

\begin{equation} T_n = \begin{bmatrix} a^2 + b^2 - c^2 - d^2 & 2 (b \cdot c - a \cdot d) & 2 (b \cdot d + a \cdot c) & 0 \\ 2 (b \cdot c + a \cdot d ) & a^2 - b^2 + c^2 -d^2 & 2 ( c \cdot d - a \cdot b ) & 0 \\ 2 ( b \cdot d - a \cdot c ) & 2 ( c \cdot d + a \cdot b ) & a^2 - b^2 - c^2 + d^2 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

Where:

  • \(\hat{n}\) is a unit vector that designates the direction, thus: \(\hat{n} = 1\)
  • \(\hat{n} = [x_n \quad y_n \quad z_n]^T\)
    • \(x_n\), \(y_n\), \(z_n\) are the components of vector \(\hat{n}\).
  • \(a = \cos( \theta / 2)\)
  • \(b = x_n \cdot \sin (\theta / 2)\)
  • \(c = y_n \cdot \sin (\theta / 2)\)
  • \(d = z_n \cdot \sin (\theta / 2)\)

Rotation around arbirtary axis

\begin{equation} T_n = \begin{bmatrix} x_n^2 + C \cdot (1 - x_n^2) & x_n \cdot y_n \cdot (1 - C) - z_n \cdot S & x_n \cdot z_n \cdot (1 - C) + y_n \cdot S & 0 \\ x_n \cdot y_n \cdot (1 - C) + z_n \cdot S & y_n^2 + C \cdot (1 - y_n^2) & y_n \cdot z_n \cdot (1 - C) - x_n \cdot S & 0 \\ x_n \cdot z_n \cdot (1 - C) - y_n \cdot S & y_n \cdot z_n \cdot (1 - C) + x_n \cdot S & z_n^2 + C \cdot (1 - z_n^2) & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \end{equation}

Where:

  • \(\theta\) is the rotation angle around the unit vector \(\hat{n}\)
  • \(C = \sin \theta\)
  • \(S = \cos \theta\)
  • \(\hat{n}\) is a unit vector that designates the direction, thus: \(\hat{n} = 1\)
  • \(\hat{n} = [x_n \quad y_n \quad z_n]^T\)
    • \(x_n\), \(y_n\), \(z_n\) are the components of vector \(\hat{n}\).

Testing formula in Sympy - Python CAS (Computer Algebra System):

import sympy

# t represents the angle theta
x, y, z, t = symbols('x y z t')
C, S = symbols("C S")

row1 = [ x**2 + C * (1 - x**2), x * y * (1 - C) - z * S, x * z * (1 - C)  + y * S ]
row2 = [ x * y * (1 - C) + z * S, y**2 + C * (1 - y**2), y * z * (1 - C) - x * S ]
row3 = [ x * z * (1 - C) - y * S,  y * z * (1 - C) +  x * S, z**2 + C * (1 - z**2) ]

m = Matrix([row1, row2, row3])

# C = cos(t)
# S = sin(t)

In [7]: m
Out[7]:
⎡   ⎛     2⎞    2                                          ⎤
⎢ C⋅⎝1 - x ⎠ + x     -S⋅z + x⋅y⋅(1 - C)  S⋅y + x⋅z⋅(1 - C) ⎥
⎢                                                          ⎥
⎢                       ⎛     2⎞    2                      ⎥
⎢S⋅z + x⋅y⋅(1 - C)    C⋅⎝1 - y ⎠ + y     -S⋅x + y⋅z⋅(1 - C)⎥
⎢                                                          ⎥
⎢                                           ⎛     2⎞    2  ⎥
⎣-S⋅y + x⋅z⋅(1 - C)  S⋅x + y⋅z⋅(1 - C)    C⋅⎝1 - z ⎠ + z   ⎦

# --- Determine rotation matrix around Z axis (Particular case) ------#
#

In [8]: m.subs({x: 0, y: 0, z: 1})
Out[8]:
⎡C  -S  0⎤
⎢        ⎥
⎢S  C   0⎥
⎢        ⎥
⎣0  0   1⎦

In [11]: rotZ = m.subs({x: 0, y: 0, z: 1, C: cos(t), S: sin(t) })

In [12]: rotZ
Out[12]:
⎡cos(t)  -sin(t)  0⎤
⎢                  ⎥
⎢sin(t)  cos(t)   0⎥
⎢                  ⎥
⎣  0        0     1⎦


#---- Determine rotation matrix around X axis (Particular case) ------#
#
In [14]: rotX = m.subs({x: 1, y: 0, z: 0, C: cos(t), S: sin(t) })

In [15]: rotX
Out[15]:
⎡1    0        0   ⎤
⎢                  ⎥
⎢0  cos(t)  -sin(t)⎥
⎢                  ⎥
⎣0  sin(t)  cos(t) ⎦

#---- Determine rotation matrix around Y axis (Particular case) ------#
#
In [16]: rotY = m.subs({x: 0, y: 1, z: 0, C: cos(t), S: sin(t) })

In [17]: rotY
Out[17]:
⎡cos(t)   0  sin(t)⎤
⎢                  ⎥
⎢   0     1    0   ⎥
⎢                  ⎥
⎣-sin(t)  0  cos(t)⎦

1.8.7 Rotation Matrix and Rodrigues' Rotation Formula

The Rodrigues' Rotation formula, named after the mathematician Olinde Rodrigues, allows rotating a vector in a 3D space, given an axis unit vector and a rotation angle around this axis. Variants of this formula can be used for determining the rotation matrix around any axis and for computing the axis-angle equivalent of a rotation matrix. This formula has wide variety of applications, including computer graphics, games, robotics, mechanical engineering and aerospace design.

Consider the following picture that contains a vector \('\boldsymbol{v}\) which is the position vector \(\boldsymbol{v}\) rotated by an angle \(\theta\) around an axis designated by the unite vector \(\boldsymbol{n}\). The vector \(\boldsymbol{v}_p\) is the projection of vector \(\boldsymbol{v}\) onto the vector \(\boldsymbol{n}\) and the vector \(\boldsymbol{v}_r\) is the rejection of vector \(\boldsymbol{v}\).

rodriguez-formula-rotation1.png

Figure 2: Rotation of position vector around an axis

According to the previous picture it is possible to find the following expressions. Where r is the radius of the circle, which is the same as the lenght of CA.

\(\boldsymbol{v}_p\) and \(\boldsymbol{v}_r\) that

\begin{equation} \| \boldsymbol{v}_p \| = \| v \| \cos \alpha \end{equation} \begin{equation} r = \| \boldsymbol{v}_r \| = \| v \| \sin \alpha \end{equation}

The projection vector \(\boldsymbol{v_p}\) (OC) can be determined using the following identity:

\begin{equation} v_p = (\boldsymbol{n} \cdot \boldsymbol{v}) \boldsymbol{n} \end{equation}

The cross product between vectors \(\boldsymbol{n}\) and vector \(\boldsymbol{v}\) is computed as:

\begin{equation} \| \boldsymbol{n} \times \boldsymbol{v} \| = \| \boldsymbol{n} \| \| \boldsymbol{v} \| \sin \alpha = \| \boldsymbol{v} \| \sin \alpha \end{equation}

Knowing that \(\| \boldsymbol{v}_r = \| \boldsymbol{v} \| \sin \alpha\), the following can be determined:

\begin{equation} r = \| \boldsymbol{v}_r \| = \| \boldsymbol{n} \times \boldsymbol{v} \| = \| \boldsymbol{v} \| \sin \alpha \end{equation}

The vector \(\boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v})\) is orthogonal to both \(\boldsymbol{n}\) and \(\boldsymbol{n} \times \boldsymbol{v}\) and has opposite direction to the vector \(\boldsymbol{v_r}\). The norm/magnitude of this cross product vector is determined by using the cross product identity.

\begin{equation} \| \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v} ) \| = \| \boldsymbol{n} \| \| \boldsymbol{n} \times \boldsymbol{v} \| = \| \boldsymbol{n} \times \boldsymbol{v} \| = \| \boldsymbol{v}_r \| \end{equation}

Since the vector \(\boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v})\) has the same magnitude as the vector \(\boldsymbol{v}_r\) and opposite direction, it is possible to find that.

\begin{equation} \boldsymbol{v}_r = - \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) \end{equation}

The projection vector \(\boldsymbol{v}_p\) can also be computed in terms of rejection vector \(\boldsymbol{v}_r\) by using the expression \(\boldsymbol{v} = \boldsymbol{v}_p + \boldsymbol{v}_r\).

\begin{equation} \boldsymbol{v}_p = \boldsymbol{v} - \boldsymbol{v}_r = \boldsymbol{v} - (- \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) ) \end{equation} \begin{equation} \boldsymbol{v}_p = \boldsymbol{v} + \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) \end{equation}

The unit vector \(\boldsymbol{e}_a\) that has the same direction as the vector \(\boldsymbol{v}_r\) and it can computed as:

\begin{equation} \boldsymbol{e}_a = - \frac{1}{ \| \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) \| } \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) = - \frac{1}{ \| \boldsymbol{n} \times \boldsymbol{v} \| } \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) \end{equation}

The unit vector \(\boldsymbol{e}_b\) (direction CD), the same direction of vector \(\boldsymbol{n} \times \boldsymbol{v}\) is orthogonal to \(\boldsymbol{n}\), \(\boldsymbol{v}\) and \(\boldsymbol{v}_r\) is determined by normalizing the vector \(\boldsymbol{n} \times \boldsymbol{v}\).

\begin{equation} \boldsymbol{e}_b = \frac{ \boldsymbol{n} \times \boldsymbol{v} } {\| \boldsymbol{n} \times \boldsymbol{v} \|} \end{equation}

By projecting the rotated rejection vector \('\boldsymbol{v}_r\) on the vectors \(\boldsymbol{e}_a\) and \(\boldsymbol{e}_b\), the next expression is found as follow:

\begin{equation} \boldsymbol{v}_r' = r \cos(\theta) \boldsymbol{e}_a + r \sin(\theta) \boldsymbol{e}_b \end{equation} \begin{equation} \boldsymbol{v}_r' = \| \boldsymbol{n} \times \boldsymbol{v} \| \cos(\theta) ( - \frac{1}{ \| \boldsymbol{n} \times \boldsymbol{v} \| } \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) ) + \| \boldsymbol{n} \times \boldsymbol{v} \| \sin(\theta) \frac{ \boldsymbol{n} \times \boldsymbol{v} } {\| \boldsymbol{n} \times \boldsymbol{v} \|} \end{equation} \begin{equation} \boldsymbol{v}_r' = - \cos(\theta) \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) + \sin(\theta) \boldsymbol{n} \times \boldsymbol{v} \end{equation}

The rotated vector \(\boldsymbol{v}'\) can be found by adding the rotated rejection vector \(\boldsymbol{v}_r'\) and the projection vector \(\boldsymbol{v}_p\), replacing the vector \(\boldsymbol{v}_p\) with \(\boldsymbol{v} + \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v})\) and also replacing the \(\boldsymbol{e}_a\) and \(\boldsymbol{e}_b\) with its previous values.

\begin{equation} \boldsymbol{v}' = \boldsymbol{v}_p + \boldsymbol{v}_r' = (\boldsymbol{v} + \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) ) + ( - \cos(\theta) \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) + \sin(\theta) \boldsymbol{n} \times \boldsymbol{v} ) \end{equation} \begin{equation} \boldsymbol{v}' = \boldsymbol{v} + \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) - \cos(\theta) \boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) + \sin(\theta) \boldsymbol{n} \times \boldsymbol{v} \end{equation}

Finally, by further simplyfing the previous expression, it possible to find the Rodriguez's formula for rotation:

\begin{equation} \boxed{ \boldsymbol{v}' = \boldsymbol{v} + (1 - \cos \theta )\boldsymbol{n} \times (\boldsymbol{n} \times \boldsymbol{v}) + ( \sin \theta ) \boldsymbol{n} \times \boldsymbol{v} } \end{equation}

The cross product can be written as a matrix multiplication by using the cross product matrix \([\boldsymbol{u}]_{\times}\) operator defined as:

\begin{equation} [\boldsymbol{u}]_{\times} = \begin{pmatrix} 0 & -u_z & u_y \\ u_z & 0 & -u_x \\ -u_y & u_x & 0 \\ \end{pmatrix} \end{equation}

Cross-product represented as matrix multiplication.

\begin{equation} \boldsymbol{n} \times \boldsymbol{v} = [ \boldsymbol{n} ]_{\times} \boldsymbol{v} \end{equation}

The Rodriguez's formula for rotation can be formulated as a matrix multiplication by replacing cross-product operations with matrix multiplication where \(\boldsymbol{I}\) is a 3 x 3 identity matrix.

\begin{equation} \boldsymbol{v}' = \boldsymbol{v} + (1 - \cos \theta ) [ \boldsymbol{n}]_{\times} ( [\boldsymbol{n}]_{\times} \boldsymbol{v}) + ( \sin \theta ) [ \boldsymbol{n}]_{\times} \boldsymbol{v} \end{equation} \begin{equation} \boldsymbol{v}' = \boldsymbol{I} \boldsymbol{v} + (1 - \cos \theta ) [\boldsymbol{n}]_{\times}^2 \boldsymbol{v} + ( \sin \theta ) [\boldsymbol{n}]_{\times} \boldsymbol{v} \end{equation} \begin{equation} \boldsymbol{v}' = \left( \boldsymbol{I} + (1 - \cos \theta ) [\boldsymbol{n}]_{\times}^2 + ( \sin \theta ) [\boldsymbol{n}]_{\times} \right) \boldsymbol{v} \end{equation}

So, the rotation matrix R such that \(\boldsymbol{v}' = R \boldsymbol{v}\) can be found as the following expression, where \(N = [\boldsymbol{n}]_{\times}\) is the cross product matrix of the rotation axis vector and \(\boldsymbol{I}\) is a 3 x 3 identity matrix. In computer graphics, it is more efficient to compute the rotation matrix only once and use to rotate all vertices of an object on the GPU side instead of applying the Rodriguez's formula to every object vertex.

\begin{equation} R = \boldsymbol{I} + (\sin \theta) [\boldsymbol{n}]_{\times} + (1 - \cos \theta ) [\boldsymbol{n}]_{\times}^2 \end{equation} \begin{equation} \boxed{ R = \boldsymbol{I} + (\sin \theta) N + (1 - \cos \theta ) N^2 } \end{equation}

Deriving the Rotation Matrix in Wolfram Mathemtica

The general rotation matrix R can be easily found by using a CAS - Computer Algebra System such as Wolfram Mathematica, SymPy (Python) library or GNU Maxima. In this case, it is used Wolfram Mathematica for determining this matrix due to its ease of use and its rule-rewriting systems that makes it easier to manipulate math formulas and generate LaTex, Mathml, ascii representation, C, C++ or Fortran code. An alternative CAS is mathics, a free and open source alternative implementation of Wolfram programming language, built on top of Python's sympy library for symbolic math computation.

Run Mathics => Mathematica Computer Algebra System clone for Python:

Run interactive shell on command line:

docker run --rm -it --security-opt=seccomp=unconfined \
        --name=mathics-cli --volume=/tmp:/usr/src/app/data \
        mathicsorg/mathics --mode cli

Mathicscript: 6.0.0, Mathics 6.0.0
on CPython 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0]

Using:
SymPy 1.9, mpmath 1.2.1, numpy 1.21.5
cython Not installed, matplotlib 3.5.1,
Asymptote version 2.83

Copyright (C) 2011-2023 The Mathics Team.
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions.
See the documentation for the full license.

Quit by evaluating Quit[] or by pressing CONTROL-D.

In[1]:=

Run graphics notebook - web interface on port 9080 using docker. After the container initialization, open the URL http://localhost:9080 on the Web browser.

$  docker run --rm -it --security-opt=seccomp=unconfined --name=mathics-cli -p=9080:8000 \
     --volume=/tmp:/usr/src/app/data mathicsorg/mathics  --mode ui

Wolfram Mathematica or Mathics session:

(* Define the rotation axis *)
n = {nx, ny, nz}

(* Turns a vector in to its cross product matrix *)
cross[u_] := {{0, -u[[3]], u[[2]] }, { u[[3]], 0, -u[[1]]}, {-u[[2]], u[[1]], 0} }


In[6]:= I3 = IdentityMatrix[3]

Out[6]= {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}

In[7]:= I3 // MatrixForm

Out[7]//MatrixForm= 1   0   0

                    0   1   0

                    0   0   1

 In[8]:= xN = cross[n] (* Cross product matrix of rotation axis n*)

 Out[8]= {{0, -nz, ny}, {nz, 0, -nx}, {-ny, nx, 0}}

 In[9]:= xN // MatrixForm

 Out[9]//MatrixForm= 0     -nz   ny

                     nz    0     -nx

                     -ny   nx    0


 In[10]:= R = I3 + S * xN + (1 - C) * (xN . xN)

                            2     2
 Out[10]= {{1 + (1 - C) (-ny  - nz ), (1 - C) nx ny - nz S, 

                                                                     2     2
 >     (1 - C) nx nz + ny S}, {(1 - C) nx ny + nz S, 1 + (1 - C) (-nx  - nz ), 

 >     (1 - C) ny nz - nx S}, {(1 - C) nx nz - ny S, (1 - C) ny nz + nx S, 

                       2     2
 >     1 + (1 - C) (-nx  - ny )}}

 In[11]:= 
 In[11]:= R = I3 + S * xN + (1 - C) * MatrixPower[xN, 2] (* Note: a^2 - does not work with matrices *)

                            2     2
 Out[11]= {{1 + (1 - C) (-ny  - nz ), (1 - C) nx ny - nz S, 

                                                                     2     2
 >     (1 - C) nx nz + ny S}, {(1 - C) nx ny + nz S, 1 + (1 - C) (-nx  - nz ), 

 >     (1 - C) ny nz - nx S}, {(1 - C) nx nz - ny S, (1 - C) ny nz + nx S, 

                       2     2
 >     1 + (1 - C) (-nx  - ny )}}



 rotX = { nx -> 1, ny -> 0, nz -> 0,  C -> Cos[ \[Theta] ], S -> Sin[ \[Theta] ] };
 rotY = { nx -> 0, ny -> 1, nz -> 0,  C -> Cos[ \[Theta] ], S -> Sin[ \[Theta] ] };
 rotZ = { nx -> 0, ny -> 0, nz -> 1,  C -> Cos[ \[Theta] ], S -> Sin[ \[Theta] ] };


 (* --------------- Rotation Matrix around X axis  -------------------------------*)

 In[26]:= Rx = R /. rotX

 Out[26]= {{1, 0, 0}, {0, Cos[θ], -Sin[θ]}, {0, Sin[θ], Cos[θ]}}

 In[27]:= Rx // MatrixForm

 Out[27]//MatrixForm= 1          0          0

                      0          Cos[θ]    -Sin[θ]

                      0          Sin[θ]    Cos[θ]

 In[28]:= Rx // MatrixForm // TeXForm

 Out[28]//TeXForm= \left(
                   \begin{array}{ccc}
                    1 & 0 & 0 \\
                    0 & \cos (\theta ) & -\sin (\theta ) \\
                    0 & \sin (\theta ) & \cos (\theta ) \\
                   \end{array}
                   \right)

 (* --------------- Rotation Matrix around Y axis  -------------------------------*)

 In[31]:= Ry = R /. rotY

 Out[31]= {{Cos[θ], 0, Sin[θ]}, {0, 1, 0}, {-Sin[θ], 0, Cos[θ]}}

 In[32]:= Ry // MatrixForm

 Out[32]//MatrixForm= Cos[θ]    0          Sin[θ]

                      0          1          0

                      -Sin[θ]   0          Cos[θ]

 In[33]:= Ry // MatrixForm // TeXForm

 Out[33]//TeXForm= \left(
                   \begin{array}{ccc}
                    \cos (\theta ) & 0 & \sin (\theta ) \\
                    0 & 1 & 0 \\
                    -\sin (\theta ) & 0 & \cos (\theta ) \\
                   \end{array}
                   \right)

 (* --------------- Rotation Matrix around Z axis  -------------------------------*)

 In[34]:= Rz = R /. rotZ

 Out[34]= {{Cos[θ], -Sin[θ], 0}, {Sin[θ], Cos[θ], 0}, {0, 0, 1}}

 In[35]:= Rz // MatrixForm 

 Out[35]//MatrixForm= Cos[θ]    -Sin[θ]   0

                      Sin[θ]    Cos[θ]    0

                      0          0          1

 In[36]:= Rz // MatrixForm // TeXForm

 Out[36]//TeXForm= \left(
                   \begin{array}{ccc}
                    \cos (\theta ) & -\sin (\theta ) & 0 \\
                    \sin (\theta ) & \cos (\theta ) & 0 \\
                    0 & 0 & 1 \\
                   \end{array}
                   \right)

 (* -------------- General Rotation Matrix --------------------------------------- *)

In[41]:= Clear[n]
In[42]:= Rp = R /. { nx -> Subscript[n, x], ny -> Subscript[n, y], nz -> Subscript[n, z] };

In[44]:= Rp // MatrixForm // TeXForm

Out[44]//TeXForm= 
  \left(
  \begin{array}{ccc}
   (1-C) \left(-n_y^2-n_z^2\right)+1 & (1-C) n_x n_y-S n_z & (1-C) n_x n_z+S
     n_y \\
   (1-C) n_x n_y+S n_z & (1-C) \left(-n_x^2-n_z^2\right)+1 & (1-C) n_y n_z-S
     n_x \\
   (1-C) n_x n_z-S n_y & (1-C) n_y n_z+S n_x & (1-C)
     \left(-n_x^2-n_y^2\right)+1 \\
  \end{array}
  \right)


 In[52]:= R2 = R /. {-ny^2 - nz^2 -> nx^2 - 1, -nx^2 - nz^2 -> ny^2 - 1, -nx^2 - ny^2 -> nz^2 - 1 }

                                2
 Out[52]= {{1 + (1 - C) (-1 + nx ), (1 - C) nx ny - nz S, 

                                                                         2
 >     (1 - C) nx nz + ny S}, {(1 - C) nx ny + nz S, 1 + (1 - C) (-1 + ny ), 

 >     (1 - C) ny nz - nx S}, {(1 - C) nx nz - ny S, (1 - C) ny nz + nx S, 

                           2
 >     1 + (1 - C) (-1 + nz )}}


 In[53]:= Rp2 = R2 /. { nx -> Subscript[n, x], ny -> Subscript[n, y], nz -> Subscript[n, z] };

 In[54]:= Rp2 

                                2
 Out[54]= {{1 + (1 - C) (-1 + n  ), (1 - C) n  n  - S n , 
                               x             x  y      z

                                                                         2
 >     S n  + (1 - C) n  n }, {(1 - C) n  n  + S n , 1 + (1 - C) (-1 + n  ), 
          y            x  z             x  y      z                     y

 >     -(S n ) + (1 - C) n  n }, 
            x             y  z

                                                                          2
 >    {-(S n ) + (1 - C) n  n , S n  + (1 - C) n  n , 1 + (1 - C) (-1 + n  )}}
            y             x  z     x            y  z                     z


 In[55]:= Rp2 // MatrixForm // TeXForm

 Out[55]//TeXForm= 
    \left(
    \begin{array}{ccc}
     (1-C) \left(n_x^2-1\right)+1 & (1-C) n_x n_y-S n_z & (1-C) n_x n_z+S n_y
       \\
     (1-C) n_x n_y+S n_z & (1-C) \left(n_y^2-1\right)+1 & (1-C) n_y n_z-S n_x
       \\
     (1-C) n_x n_z-S n_y & (1-C) n_y n_z+S n_x & (1-C) \left(n_z^2-1\right)+1
       \\
    \end{array}
    \right)


The overall rotation matrix around an arbitrary axis is given by the following expression where \(n_x\), \(n_y\) and \(n_z\) are components of the rotation axis vector \(\boldsymbol{n}\) (unitary vector); \(\theta\) is the rotation angle; \(C = \sin \theta\); and \(S = \sin \theta\).

\begin{equation} R = \begin{bmatrix} (1-C) \left(-n_y^2-n_z^2\right)+1 & (1-C) n_x n_y-S n_z & (1-C) n_x n_z+S n_y \\ (1-C) n_x n_y+S n_z & (1-C) \left(-n_x^2-n_z^2\right)+1 & (1-C) n_y n_z-S n_x \\ (1-C) n_x n_z-S n_y & (1-C) n_y n_z+S n_x & (1-C) \left(-n_x^2-n_y^2\right)+1 \\ \end{bmatrix} \end{equation}

The general rotation matrix can also be expressed in this form that is obtained by replacing \(n_x^2 - 1 = - (n_y^2 + nz^2)\), \(n_y^2 - 1 = - (n_x^2 + nz^2)\) and \(n_z^2 - 1 = - (n_x^2 + ny^2)\).

\begin{equation} \boxed{ R = \begin{bmatrix} (1-C) \left(n_x^2-1\right)+1 & (1-C) n_x n_y-S n_z & (1-C) n_x n_z+S n_y \\ (1-C) n_x n_y+S n_z & (1-C) \left(n_y^2-1\right)+1 & (1-C) n_y n_z-S n_x \\ (1-C) n_x n_z-S n_y & (1-C) n_y n_z+S n_x & (1-C) \left(n_z^2-1\right)+1 \\ \end{bmatrix} } \end{equation}

Rotation matrix around X axis.

\begin{equation} \boxed{ R_x = \left( \begin{array}{ccc} 1 & 0 & 0 \\ 0 & \cos (\theta ) & -\sin (\theta ) \\ 0 & \sin (\theta ) & \cos (\theta ) \\ \end{array} \right) } \end{equation}

Rotation matrix around Y axis.

\begin{equation} \boxed{ R_y = \left( \begin{array}{ccc} \cos (\theta ) & 0 & \sin (\theta ) \\ 0 & 1 & 0 \\ -\sin (\theta ) & 0 & \cos (\theta ) \\ \end{array} \right) } \end{equation}

Rotation matrix around Z axis.

\begin{equation} \boxed{ R_z = \left( \begin{array}{ccc} \cos (\theta ) & -\sin (\theta ) & 0 \\ \sin (\theta ) & \cos (\theta ) & 0 \\ 0 & 0 & 1 \\ \end{array} \right) } \end{equation}

Rodriguez Formula in Alternative Format

From previous equation, the unit vector \(\boldsymbol{e}_a\) can be found as:

\begin{equation} \boldsymbol{e}_a = \frac{ \boldsymbol{v}_r }{ \| \boldsymbol{v}_r \| } = \frac{\boldsymbol{v} - (\boldsymbol{v} \cdot \boldsymbol{n}) \boldsymbol{n}} { \| \boldsymbol{n} \times \boldsymbol{v} \| } \end{equation} \begin{equation} \boldsymbol{v}_r' = r \cos(\theta) \boldsymbol{e}_a + r \sin(\theta) \boldsymbol{e}_b \end{equation} \begin{equation} \boldsymbol{v}_r' = \| \boldsymbol{n} \times \boldsymbol{v} \| \cos(\theta) ( \frac{\boldsymbol{v} - (\boldsymbol{v} \cdot \boldsymbol{n}) \boldsymbol{n}} { \| \boldsymbol{n} \times \boldsymbol{v} \| } ) + \| \boldsymbol{n} \times \boldsymbol{v} \| \sin(\theta) \frac{ \boldsymbol{n} \times \boldsymbol{v} } {\| \boldsymbol{n} \times \boldsymbol{v} \|} \end{equation} \begin{equation} \boldsymbol{v}_r' = \cos(\theta) ( \boldsymbol{v} - (\boldsymbol{v} \cdot \boldsymbol{n}) \boldsymbol{n} ) + \sin(\theta) \boldsymbol{n} \times \boldsymbol{v} \end{equation}

Then, rotated vector \(\boldsymbol{v}'\) can be found by replacing the values of previous equations in the next equation.

\begin{equation} \boldsymbol{v}' = \boldsymbol{v}_p + \boldsymbol{v}_r' \end{equation} \begin{equation} \boldsymbol{v}' = (\boldsymbol{v} \cdot \boldsymbol{n}) \boldsymbol{n} + ( \cos(\theta) ( \boldsymbol{v} - (\boldsymbol{v} \cdot \boldsymbol{n}) \boldsymbol{n} ) + \sin(\theta) \boldsymbol{n} \times \boldsymbol{v} ) \end{equation}

Alternative Format of Roriguez Formula: Finally, the alternative format of Rodriguez Formula for rotation can be determined as follow.

\begin{equation} \boldsymbol{v}' = \boldsymbol{v} \cos \theta + (1 - \cos \theta) (\boldsymbol{v} \cdot \boldsymbol{n}) \boldsymbol{n} + (\boldsymbol{n} \times \boldsymbol{v}) \sin \theta \end{equation}

It can be found that the previous expression can be turned into a matrix multiplication as in shown in the next equation where \(a^T\) is the tranpose of matrix a; I 3x3 identiy matrix; \(N = [\boldsymbol{b}]_{\times}\) is the cross product matrix of rotation axis vector \(\boldsymbol{n}\). In this case, the vectors \(\boldsymbol{v}\), \(\boldsymbol{v}'\) and \(\boldsymbol{n}\) are treated as 3 x 1 (3 by 1) column matrices.

\begin{equation} \boldsymbol{v}' = I \boldsymbol{v} \cos \theta + (1 - \cos \theta)(\boldsymbol{n} \boldsymbol{n}^T) \boldsymbol{v} + (N \sin \theta) \boldsymbol{v} \end{equation} \begin{equation} \boldsymbol{v}' = I \boldsymbol{v} \cos \theta + (1 - \cos \theta)(\boldsymbol{n} \boldsymbol{n}^T) \boldsymbol{v} + (N \sin \theta) \boldsymbol{v} \end{equation} \begin{equation} \boldsymbol{v}' = \left( I \cos \theta + (1 - \cos \theta)\boldsymbol{n} \boldsymbol{n}^T + N \sin(\theta) \right) \boldsymbol{v} \end{equation} \begin{equation} \boldsymbol{v}' = \left( \boldsymbol{n} \boldsymbol{n}^T + (I - \boldsymbol{n} \boldsymbol{n}^T ) \cos \theta + N \sin(\theta) \right) \boldsymbol{v} \end{equation}

So, the general rotation matrix R that represents a rotation around an arbitrary axis such that \(\boldsymbol{v}' = R \boldsymbol{v}\) can be found as:

\begin{equation} R = \boldsymbol{n} \boldsymbol{n}^T + (I - \boldsymbol{n} \boldsymbol{n}^T ) \cos \theta + N \sin \theta \end{equation}

The rotation matrix can be determined from this expression by using Wolfram Mathematica computer algebra system.

In[1]:= n = { {nx}, {ny}, {nz} }

Out[1]= {{nx}, {ny}, {nz}}

In[2]:= Dimensions[n]

Out[2]= {3, 1}

In[3]:= n // MatrixForm

Out[3]//MatrixForm= nx

                    ny

                    nz

In[4]:= P = n . Transpose[n]

            2                           2                           2
Out[4]= {{nx , nx ny, nx nz}, {nx ny, ny , ny nz}, {nx nz, ny nz, nz }}

In[5]:= P // MatrixForm

Out[5]//MatrixForm=   2
                    nx      nx ny   nx nz

                              2
                    nx ny   ny      ny nz

                                      2
                    nx nz   ny nz   nz


In[6]:= I3 = IdentityMatrix[3]

Out[6]= {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}

In[7]:= SN = {{0, -nz, ny}, {nz, 0, -nx}, {-ny, nx, 0}};

(* C - represents cos(t), S - represents sin(t) *)
In[8]:= R = P + (I3 - P) * C + SN * S 


In[9]:= R // MatrixForm

Out[9]//MatrixForm= 

>     2            2
    nx  + C (1 - nx )        nx ny - C nx ny - nz S   nx nz - C nx nz + ny S

                               2            2
    nx ny - C nx ny + nz S   ny  + C (1 - ny )        ny nz - C ny nz - nx S

                                                        2            2
    nx nz - C nx nz - ny S   ny nz - C ny nz + nx S   nz  + C (1 - nz )


rotX = { nx -> 1, ny -> 0, nz -> 0,  C -> Cos[ \[Theta] ], S -> Sin[ \[Theta] ] };
rotY = { nx -> 0, ny -> 1, nz -> 0,  C -> Cos[ \[Theta] ], S -> Sin[ \[Theta] ] };
rotZ = { nx -> 0, ny -> 0, nz -> 1,  C -> Cos[ \[Theta] ], S -> Sin[ \[Theta] ] };


In[14]:= Rx = R /. rotX

Out[14]= {{1, 0, 0}, {0, Cos[θ], -Sin[θ]}, {0, Sin[θ], Cos[θ]}}


In[15]:= Rx // MatrixForm

Out[15]//MatrixForm= 1          0          0

                     0          Cos[θ]    -Sin[θ]

                     0          Sin[θ]    Cos[θ]


In[16]:= Ry = R /. rotY

Out[16]= {{Cos[θ], 0, Sin[θ]}, {0, 1, 0}, {-Sin[θ], 0, Cos[θ]}}

In[17]:= MatrixForm[Ry]

Out[17]//MatrixForm= Cos[θ]    0          Sin[θ]

                     0          1          0

                     -Sin[θ]   0          Cos[θ]


In[18]:= Rz = R /. rotZ

Out[18]= {{Cos[θ], -Sin[θ], 0}, {Sin[θ], Cos[θ], 0}, {0, 0, 1}}

In[19]:= Rz // MatrixForm

Out[19]//MatrixForm= Cos[θ]    -Sin[θ]   0

                     Sin[θ]    Cos[θ]    0

                     0          0          1


In[27]:= ClearAll[n]

In[28]:= toSubscr =  { nx -> Subscript[n, x], ny -> Subscript[n, y], nz -> Subscript[n, z]};

In[29]:= Rp = R /. toSubscr;


In[31]:= Rp

             2            2
Out[31]= {{n   + C (1 - n  ), n  n  - C n  n  - S n , 
            x            x     x  y      x  y      z

                                                          2            2
>     S n  + n  n  - C n  n }, {n  n  - C n  n  + S n , n   + C (1 - n  ), 
         y    x  z      x  z     x  y      x  y      z   y            y

>     -(S n ) + n  n  - C n  n }, 
           x     y  z      y  z

                                                           2            2
>    {-(S n ) + n  n  - C n  n , S n  + n  n  - C n  n , n   + C (1 - n  )}}
           y     x  z      x  z     x    y  z      y  z   z            z


In[33]:= Rp // MatrixForm // TeXForm

Out[33]//TeXForm= 
   \left(
   \begin{array}{ccc}
    C \left(1-n_x^2\right)+n_x^2 & -C n_x n_y-S n_z+n_x n_y & -C n_x n_z+S
      n_y+n_x n_z \\
    -C n_x n_y+S n_z+n_x n_y & C \left(1-n_y^2\right)+n_y^2 & -C n_y n_z-S
      n_x+n_y n_z \\
    -C n_x n_z-S n_y+n_x n_z & -C n_y n_z+S n_x+n_y n_z & C
      \left(1-n_z^2\right)+n_z^2 \\
   \end{array}
   \right)

Finally, from Wolfram mathematica session, the rotation matrix can be determined as:

\begin{equation} \boxed{ R = \begin{bmatrix} C \left(1-n_x^2\right)+n_x^2 & -C n_x n_y-S n_z+n_x n_y & -C n_x n_z+S n_y+n_x n_z \\ -C n_x n_y+S n_z+n_x n_y & C \left(1-n_y^2\right)+n_y^2 & -C n_y n_z-S n_x+n_y n_z \\ -C n_x n_z-S n_y+n_x n_z & -C n_y n_z+S n_x+n_y n_z & C \left(1-n_z^2\right)+n_z^2 \\ \end{bmatrix} } \end{equation}

Computing the General Rotation Matrix using Sympy

Install Python Sympy library:

$ pip3 install --user sympy

Sympy import:

from sympy import * 
init_printing('utf-8')

Define symbolic variables:

nx, ny, nz, q, C, S = symbols("nx ny nz theta C S")

Define function that turns vector into cross-product matrix.

def crossm(v):
    "Generate cross-product matrix"
    x = v[0]
    y = v[1]
    z = v[2]
    m = Matrix([[0, -z, y], [z, 0, -x], [-y, x, 0]])
    return m

Identity matrix.

>>> I3 = Matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]])

>>> I3
⎡1  0  0⎤
⎢       ⎥
⎢0  1  0⎥
⎢       ⎥
⎣0  0  1⎦

Define rotation axis unit vector \(\mathbf{n}\)

>>> n = Matrix([nx, ny, nz])

>>> n
⎡nx⎤
⎢  ⎥
⎢ny⎥
⎢  ⎥
⎣nz⎦

Cross-product matrix \([\mathbf{n}]_{\times}\), equivalent to the the cross product operator \([\mathbf{u}_{\times}] \mathbf{v} = \mathbf{u} \times \mathbf{v}\).

>>> xN = crossm(n)

>>> xN
⎡ 0   -nz  ny ⎤
⎢             ⎥
⎢nz    0   -nx⎥
⎢             ⎥
⎣-ny  nx    0 ⎦

Check the cross-product matrix is correctness \(\mathbf{n} \times \mathbf{n} = [\mathbf{n}]_{\times} \mathbf{n} = \mathbf{0}\). The product of the cross-product matrix of a vector by itself should be a zero vector. Note: The matrix multiplication operator of Sympy is (*), not (@) 'at' like Python's numpy.

>>> xN * n
⎡0⎤
⎢ ⎥
⎢0⎥
⎢ ⎥
⎣0⎦

Compute the general rotation matrix.

\begin{equation*} R = \boldsymbol{n} \boldsymbol{n}^T + (I - \boldsymbol{n} \boldsymbol{n}^T ) \cos \theta + [\mathbf{n}]_{\times} \sin \theta \end{equation*}
>>> R = n * n.T + (I3 - n * n.T) * C + xN * S

>>> R
⎡     ⎛      2⎞     2                                                     ⎤
⎢   C⋅⎝1 - nx ⎠ + nx      -C⋅nx⋅ny - S⋅nz + nx⋅ny  -C⋅nx⋅nz + S⋅ny + nx⋅nz⎥
⎢                                                                         ⎥
⎢                              ⎛      2⎞     2                            ⎥
⎢-C⋅nx⋅ny + S⋅nz + nx⋅ny     C⋅⎝1 - ny ⎠ + ny      -C⋅ny⋅nz - S⋅nx + ny⋅nz⎥
⎢                                                                         ⎥
⎢                                                       ⎛      2⎞     2   ⎥
⎣-C⋅nx⋅nz - S⋅ny + nx⋅nz  -C⋅ny⋅nz + S⋅nx + ny⋅nz     C⋅⎝1 - nz ⎠ + nz    ⎦

Generate latex code of the rotation matrix \(R(\mathbf{n}, \theta)\):

>>> R_tex = latex(R).replace("nx", "n_x").replace("ny", "n_y").replace("nz", "n_z")

>>> print(R_tex)
\left[\begin{matrix}C \left(1 - n_x^{2}\right) + n_x^{2} 
              & - C n_x n_y - S n_z + n_x n_y 
               & - C n_x n_z + S n_y + n_x n_z \\ 
              - C n_x n_y + S n_z + n_x n_y & C 
           \left(1 - n_y^{2}\right) + n_y^{2} 
          & - C n_y n_z - S n_x + n_y n_z\\- C n_x n_z - S n_y + n_x n_z 
        & - C n_y n_z + S n_x + n_y n_z 
      & C \left(1 - n_z^{2}\right) + n_z^{2}\end{matrix}\right]


\begin{equation*} R(\mathbf{n}, \theta) = \left[\begin{matrix}C \left(1 - n_x^{2}\right) + n_x^{2} & - C n_x n_y - S n_z + n_x n_y & - C n_x n_z + S n_y + n_x n_z \\ - C n_x n_y + S n_z + n_x n_y & C \left(1 - n_y^{2}\right) + n_y^{2} & - C n_y n_z - S n_x + n_y n_z\\- C n_x n_z - S n_y + n_x n_z & - C n_y n_z + S n_x + n_y n_z & C \left(1 - n_z^{2}\right) + n_z^{2}\end{matrix}\right] \end{equation*}

Rotation matrix around X axis \(R_x(\theta)\)

>>> Rx = R.subs({nx: 1, ny: 0, nz: 0, C: cos(q), S: sin(q)})

>>> Rx
⎡1    0        0   ⎤
⎢                  ⎥
⎢0  cos(θ)  -sin(θ)⎥
⎢                  ⎥
⎣0  sin(θ)  cos(θ) ⎦

Rotation matrix around Y axis \(R_y(\theta)\)

>>> Ry = R.subs({nx: 0, ny: 1, nz: 0, C: cos(q), S: sin(q)})

>>> Ry
⎡cos(θ)   0  sin(θ)⎤
⎢                  ⎥
⎢   0     1    0   ⎥
⎢                  ⎥
⎣-sin(θ)  0  cos(θ)⎦

Rotation matrix around Z axis \(R_z(\theta)\):

>>> Rz = R.subs({nx: 0, ny: 0, nz: 1, C: cos(q), S: sin(q)})

>>> Rz
⎡cos(θ)  -sin(θ)  0⎤
⎢                  ⎥
⎢sin(θ)  cos(θ)   0⎥
⎢                  ⎥
⎣  0        0     1⎦

Complete Rotation Matrix:

R_ = R.subs({C: cos(q), S: sin(q)})

for i in range(0, 3):
    for j in range(0, 3):
        print(f"R({i+1},{j+1}) = "); pprint(R_[i, j]); print()

>>> for i in range(0, 3):
...     for j in range(0, 3):
...         print(f"R({i+1},{j+1}) = "); pprint(R_[i, j]); print()
... 
R(1,1) = 
  2   ⎛      2⎞       
nx  + ⎝1 - nx ⎠⋅cos(θ)

R(1,2) = 
-nx⋅ny⋅cos(θ) + nx⋅ny - nz⋅sin(θ)

R(1,3) = 
-nx⋅nz⋅cos(θ) + nx⋅nz + ny⋅sin(θ)

R(2,1) = 
-nx⋅ny⋅cos(θ) + nx⋅ny + nz⋅sin(θ)

R(2,2) = 
  2   ⎛      2⎞       
ny  + ⎝1 - ny ⎠⋅cos(θ)

R(2,3) = 
-nx⋅sin(θ) - ny⋅nz⋅cos(θ) + ny⋅nz

R(3,1) = 
-nx⋅nz⋅cos(θ) + nx⋅nz - ny⋅sin(θ)

R(3,2) = 
nx⋅sin(θ) - ny⋅nz⋅cos(θ) + ny⋅nz

R(3,3) = 
  2   ⎛      2⎞       
nz  + ⎝1 - nz ⎠⋅cos(θ)

CSE - Common Subexpression Elimination for code generation.

  • Common Subexpression Elimination is a compiler optimization that collects multiple subexpressions and evaluates them only once in order to increase the performence and avoid evaluating expression multiple times. Python's Sympy library for symbolic computing has CSE function that can be used for generating code from math formulas with subexpressions evaluated just a single time.
>>> terms = cse(R_)

>>> len(terms)
2

>>> len(terms[0])
14

>>> len(terms[1])
1

>>> print(terms)
([(x0, nx**2), (x1, cos(theta)), (x2, sin(theta)), (x3, nz*x2), 
(x4, nx*ny), (x5, x1*x4), (x6, nx*nz), (x7, ny*x2), (x8, x1*x6), 
(x9, ny**2), (x10, nx*x2), (x11, ny*nz), (x12, x1*x11), (x13, nz**2)], [Matrix([
[x0 + x1*(1 - x0),  nx*ny - x3 - x5,       x6 + x7 - x8],
[    x3 + x4 - x5, x1*(1 - x9) + x9,  ny*nz - x10 - x12],
[ nx*nz - x7 - x8,  x10 + x11 - x12, x1*(1 - x13) + x13]])])

Display CSE variables:

>>> for (var, val) in terms[0]: print(f"{var} = {val}")
... 
x0 = nx**2
x1 = cos(theta)
x2 = sin(theta)
x3 = nz*x2
x4 = nx*ny
x5 = x1*x4
x6 = nx*nz
x7 = ny*x2
x8 = x1*x6
x9 = ny**2
x10 = nx*x2
x11 = ny*nz
x12 = x1*x11
x13 = nz**2

General rotation matrix with subexpression elimination.

>>> R_cse
⎡x₀ + x₁⋅(1 - x₀)  nx⋅ny - x₃ - x₅      x₆ + x₇ - x₈   ⎤
⎢                                                      ⎥
⎢  x₃ + x₄ - x₅    x₁⋅(1 - x₉) + x₉  ny⋅nz - x₁₀ - x₁₂ ⎥
⎢                                                      ⎥
⎣nx⋅nz - x₇ - x₈   x₁₀ + x₁₁ - x₁₂   x₁⋅(1 - x₁₃) + x₁₃⎦

for i in range(0, 3):
    for j in range(0, 3):
        print(f"R({i+1},{j+1}) = {R_cse[i, j]}")

>>> for i in range(0, 3):
...     for j in range(0, 3):
...         print(f"R({i+1},{j+1}) = {R_cse[i, j]}")
... 

R(1,1) = x0 + x1*(1 - x0)
R(1,2) = nx*ny - x3 - x5
R(1,3) = x6 + x7 - x8
R(2,1) = x3 + x4 - x5
R(2,2) = x1*(1 - x9) + x9
R(2,3) = ny*nz - x10 - x12
R(3,1) = nx*nz - x7 - x8
R(3,2) = x10 + x11 - x12
R(3,3) = x1*(1 - x13) + x13

Matlab/Octave-like Pseudo Code for the Rotation Matrix with CSE:

% Initialize R as 3x3 zero matrix
function [R] rotMatrix(theta, n)
   [nx, ny, nz] = n
   % Create Intermediate Variables %
   x0 = nx**2
   x1 = cos(theta)
   x2 = sin(theta)
   x3 = nz*x2
   x4 = nx*ny
   x5 = x1*x4
   x6 = nx*nz
   x7 = ny*x2
   x8 = x1*x6
   x9 = ny**2
   x10 = nx*x2
   x11 = ny*nz
   x12 = x1*x11
   x13 = nz**2
   % Create Rotation Matrix 
   R = zeros(3, 3)
   R(1,1) = x0 + x1*(1 - x0)
   R(1,2) = nx*ny - x3 - x5
   R(1,3) = x6 + x7 - x8
   R(2,1) = x3 + x4 - x5
   R(2,2) = x1*(1 - x9) + x9
   R(2,3) = ny*nz - x10 - x12
   R(3,1) = nx*nz - x7 - x8
   R(3,2) = x10 + x11 - x12
   R(3,3) = x1*(1 - x13) + x13
end

Code for the Rotation Matrix with CSE in Python's Numpy:

import numpy as np

def rotMat(theta, n):
   nx, ny, nz = n
   # Create Intermediate Variables %
   x0 = nx**2
   x1 = np.cos(theta)
   x2 = np.sin(theta)
   x3 = nz*x2
   x4 = nx*ny
   x5 = x1*x4
   x6 = nx*nz
   x7 = ny*x2
   x8 = x1*x6
   x9 = ny**2
   x10 = nx*x2
   x11 = ny*nz
   x12 = x1*x11
   x13 = nz**2
   # Create Rotation Matrix 
   R = np.zeros((3, 3))
   R[0,0] = x0 + x1*(1 - x0)
   R[0,1] = nx*ny - x3 - x5
   R[0,2] = x6 + x7 - x8
   R[1,0] = x3 + x4 - x5
   R[1,1] = x1*(1 - x9) + x9
   R[1,2] = ny*nz - x10 - x12
   R[2,0] = nx*nz - x7 - x8
   R[2,1] = x10 + x11 - x12
   R[2,2] = x1*(1 - x13) + x13
   return R

Testing: Comparison of Sympy rotation matrix computed by Sympy and Numpy with the previous code.

# 60 degrees (pi/3 rads) rotation around X axis
>>> rotMat(np.pi / 3, [1, 0, 0]) 
[[ 1.         0.         0.       ] 
 [ 0.         0.5       -0.8660254] 
 [ 0.         0.8660254  0.5      ]]

# 60 degrees (pi/3 rads) rotation around X axis
>>> R_.subs({nx: 1, ny: 0, nz: 0, q: np.pi / 3})
⎡1          0                  0         ⎤
⎢                                        ⎥
⎢0         0.5         -0.866025403784439⎥
⎢                                        ⎥
⎣0  0.866025403784439         0.5        ⎦


# 45 degrees (PI/4 radians) rotation around Z axis 
>>> rotMat(np.pi / 4, [0, 0, 1])
 [[ 0.70710678 -0.70710678  0.        ]
  [ 0.70710678  0.70710678  0.        ]
 [ 0.          0.          1.        ]]

# 45 degrees (PI/4 radians) rotation around Z axis 
>>> R_.subs({nx: 0, ny: 0, nz: 1, q: np.pi / 4})
⎡0.707106781186548  -0.707106781186547  0⎤
⎢                                        ⎥
⎢0.707106781186547  0.707106781186548   0⎥
⎢                                        ⎥
⎣        0                  0           1⎦

1.8.8 Camera View Transform

The camera view transform matrix transforms world-space coordinates to camera-space coordinates. On old OpenGL versions, this matrix was can be constructed through the OpenGL routine gluLookAt or the GLM (OpenGL Mathematics) library routine glm::lookAt.

The GLM function glm::lookAt() has the following type signature.

glm::mat4 glm::lookAt(
                // Eye vector - camera position in world-space coordinates
                glm::vec3 const& eye

                // At vector - point to where camera is looking at
                // in world-space coordinates.
              , glm::vec3 const& at

                // Up vector - camera's orientation. Often set to vertical axis
                // or Y axis up = (x = 0, y = 1, z = 0) by default.
              , glm::vec3 const& up
              );

  // --------- Usage ------------------------//

  ... ... ... ... ...

glm::mat4 camera_View_transform = glm::lookAt(  eye_camera_current_position
                                              , point_to_where_camera_is_looking_at
                                              , up_vector
                                              );

  // Get shader ID of shader's view transform uniform variable.
  const GLint u_view_transform = glGetUniformLocation(prog, "u_view_transform");

  // Set View tranform shader uniform variable
  glUniformMatrix4fv(u_view_transform, 1, GL_FALSE, glm::value_ptr(camera_View_transform) );

This function has the following algorithm:

\begin{equation} T_{\text{view}} = \begin{bmatrix} X_x & X_y & X_z & -\vec{X} \cdot \vec{\text{eye}} \\ Y_x & Y_y & Y_z & -\vec{Y} \cdot \vec{\text{eye}} \\ Z_x & Z_y & Z_z & -\vec{Z} \cdot \vec{\text{eye}} \\ 0 & 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} X_x & X_y & X_z & - \text{dot}(\vec{X}, \vec{\text{eye}}) \\ Y_x & Y_y & Y_z & - \text{dot}(\vec{Y}, \vec{\text{eye}}) \\ Z_x & Z_y & Z_z & - \text{dot}(\vec{Z}, \vec{\text{eye}}) \\ 0 & 0 & 0 & 1 \end{bmatrix} \end{equation}

And the unit vectors, X, Y and Z are computed in the following manner:

\begin{eqnarray*} \vec{Z} &=& \frac{ \vec{eye} - \vec{at} }{\left\| \vec{eye} - \vec{at} \right\| } = \frac{ -\vec{\text{forward}} }{\left\| \vec{\text{forward}} \right\| } = -\text{normalize}( \vec{eye} - \vec{at} ) = -\text{normalize}( \vec{\text{forward}} ) \\ \vec{X} &=& \frac{ \vec{up} \times \vec{Z} }{\left\| \vec{up} \times \vec{Z} \right\| } = \text{normalize}( \text{cross}( \vec{up}, \vec{Z} ) ) \\ \vec{Y} &=& \frac{ \vec{Z} \times \vec{X} }{\left\| \vec{Z} \times \vec{X} \right\| } = \text{normalize}( \text{cross}( \vec{Z}, \vec{X} ) ) \\ \end{eqnarray*}

The vector \(\vec{Z}\) is the opposite direction, represented by a unit vector, to where the camera is looking at. If this vector were an input, and was directly set by the calling code, rotating that camera viewing direction would require just rotating this vector.

Where:

  • \(\vec{forward} = \vec{at} - \vec{eye}\) => Direction to where the camera is looking at (a.k.a pointing at).
  • INPUTS:
    • \(\vec{eye}\) (Eye vector) => Camera's position in world-space coordinates.
    • \(\vec{at}\) (At vector) => Point in world-space coordinate where the camera is lookin at.
    • \(\vec{up}\) (Up vector) => Camera orientation, often set by default to Y axis.
  • Intermediate Outputs:
    • \(\vec{X} = [X_x \quad X_y \quad X_z]\)
    • \(\vec{Y} = [Y_x \quad Y_y \quad Y_z]\)
    • \(\vec{Z} = [Z_x \quad Z_y \quad Z_z]\)
  • Notation Used:
    • \(\left\| \vec{vector} \right\|\) - means the vector norm
    • \(\vec{A} \times \vec{B}\) - means vector cross product
    • \(\vec{A} \cdot \vec{B}\) - means vector dot product.

The previous matrix turns world-space into camera-space coordinates in the following mode:

\begin{equation} \begin{bmatrix} x_{\text{camera}} \\ y_{\text{camera}} \\ z_{\text{camera}} \\ 1 \end{bmatrix} = T_{\text{projection}} \cdot \begin{bmatrix} x_{\text{world}} \\ y_{\text{world}} \\ z_{\text{world}} \\ 1 \end{bmatrix} \end{equation}

Where:

  • The right-side (4 x 1) column vector (input) is the world-space coordinate.
  • The left-side (4 x 1) column vector (ouput) is the camera-space coordinate.

Pointing camera to specific direction

The function lookAt() points the camera view to a specific point in the space at vector in world-space coordinates. The following function could be defined for pointing the camera at specific direction (vector or normalized vector) instead of a point-vector.

glm::mat4
lookAt_direction(glm::vec3 const& eye, glm::vec3 const& forward, glm::vec3 const& up  )
{
    glm::vec3 at = eye + forward;
    return lookAt(eye, at, up);
}

Where:

  • eye - (point-verctor) Camera position in world-space coordinates
  • forward - (vector) Direction to where camera is looking at.
  • up (vector) Camera's orientation

Algorithm Validation

Testing GLM (OpenGL Mathematic Library) in CERN's root REPL:

// -------------- Case  1 --------------------- //
//
auto eye = glm::vec3(3.0, 10.0, 20.0);
auto at  = glm::vec3(50.0, 25.0, 10.0);
auto up  = glm::vec3(0.0, 1.0, 0.0);

root [108] auto view_matrix = glm::lookAt(eye, at, up);
root [109]
root [109] show_matrix("view = ", view_matrix)

 [MATRIX] view =  =
   0.208  -0.000   0.978 -20.186
  -0.291   0.955   0.062  -9.912
  -0.934  -0.298   0.199   1.808
   0.000   0.000   0.000   1.000
root [110]

// ------------ Case 2 --------------------------//
auto eye = glm::vec3(50.0, -10.0, 0.0);
auto up = glm::vec3(0.0, 1.0, 0.0);
auto at = glm::vec3(25.0, 5.615, -200.0);
auto view_matrix = glm::lookAt(eye, at, up);

root [114]
root [114] show_matrix("view = ", view_matrix)

 [MATRIX] view =  =
   0.992   0.000  -0.124 -49.614
   0.010   0.997   0.077   9.491
   0.124  -0.077   0.989  -6.956
   0.000   0.000   0.000   1.000

Implement glm::lookAt() algorithm in Julia programming language:

# Return vector v normalized =>> Returns column vector
normalize(v) = begin
   x, y, z = v
   mag = sqrt(x * x + y * y + z * z)
   [ (x / mag) (y / mag) (z / mag)]'
end

# Vector cross product =>> Returns column vector
function cross(va, vb)
   x, y, z = va
   T = [ 0 -z y ; z 0 -x; -y x 0]
   return T * vb
end

# Vector Dot product via vector column X transpose multiplication
dot(va, vb) = va' * vb

# Note: The inputs must be column vectors
function lookAt(eye, at, up)
   # The vector Z is the direction to where the camera is looking at.
   Z = normalize( eye - at      )
   X = normalize( cross(up, Z)  )
   Y = normalize( cross(Z, X )  )
   t = [ dot(eye, X)  dot(eye, Y)  dot(eye, Z) ]

   # Define identity matrix
   matrix = [ 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]

   # Assign vector X to matrix's first row
   matrix[1, 1:3] = X
   # Assign vector Y to matrix's second row
   matrix[2, 1:3] = Y
   # Assign vector Z to matrix's third row
   matrix[3, 1:3] = Z
   # Assign forth (last) column to vector t
   matrix[1:3, 4] = -t
   return matrix
end

Test implementation:

#  --------- Test Case 1 --------------------------#
# =================================================#
eye = [3.0 10.0 20.0  ]'
at  = [50.0 25.0 10.0 ]'
up  = [0.0   1.0 0.00 ]' # Y axis

julia> eye
3×1 LinearAlgebra.Adjoint{Float64,Array{Float64,2}}:
  3.0
 10.0
 20.0

# Camera's view transform matrix
julia> Tview = lookAt(eye, at, up)
4×4 Array{Float64,2}:
 -0.208108  0.0       -0.978106   20.1864
 -0.291457  0.954572   0.062012   -9.91159
  0.933672  0.297981  -0.198654  -52.1466
  0.0       0.0        0.0         1.0

# Camera's view transform matrix rounded with 3 decimal digits
julia> map(x -> round(x, digits=3), Tview)
4×4 Array{Float64,2}:
  0.208   0.0    0.978  -20.186
 -0.291   0.955  0.062   -9.912
 -0.934  -0.298  0.199    1.808
  0.0     0.0    0.0      1.0

#  --------- Test Case 2 --------------------------#
# =================================================#
eye = [50.0 -10.0 0.0 ]'
up  = [0.0 1.0 0.0 ]'
at  = [25.0 5.615 -200.0]'

Tview = lookAt(eye, at, up)

julia> map(x -> round(x, digits=3), Tview)
4×4 Array{Float64,2}:
 0.992   0.0    -0.124  -49.614
 0.01    0.997   0.077    9.491
 0.124  -0.077   0.989   -6.956
 0.0     0.0     0.0      1.0

References

1.8.9 Camera Projection Transform

The camera's projection transform matrix turns camera-space coordinates into clip-space coordinates, which are normalized NDC (Normalized-Device Coordinates). Any coordinate in the clip-space coordinate out of the range (-1.0 to 1.0) in the three axis X, Y and Z will not be shown at the screen, they will be clipped. It is worth noting that some projection matrices are not affine transforms.

Ortographic Projection Transform - (CSE167 - page 25) ; (Microsoft- WGL)

The orthographic perspective transformation preserves parallel lines and provides no sense of depth. This type of projection is suitable for charts, engineering drawing, engineering blueprints, CADs (Computer Aided Design) views and so on.

  • Computed with subroutines:
    • glm::ortho (xmin, xmax, ymin, ymax, zNear, zFar) => GLM math library
    • glm::ortho (left, right, bottom, top, near, far)
    • glOrtho(left, right, bottom, top, near, far) => Legacy OpenGL
\begin{equation} T_{\text{ortho}} = \begin{bmatrix} \frac{ 2 }{ x_{max} - x_{min} } & 0 & 0 & -\frac{ x_{max} + x_{min} }{ x_{max} - x_{min} } \\ 0 & \frac{ 2 }{ y_{max} - y_{min} } & 0 & -\frac{ y_{max} + y_{min} }{ y_{max} - y_{min} } \\ 0 & 0 & - \frac{ 2 }{ z_{far} - z_{near} } & - \frac{ z_{far} + z_{near} }{ z_{far} - z_{near} } \\ 0 & 0 & 0 & 1 \end{bmatrix} \end{equation}

Where:

  • \(x_{min}\) (left)
  • \(x_{max}\) (right)
  • \(y_{min}\) (bottom)
  • \(y_{max}\) (top)
  • \(z_{near}\) (near) => Distance from the camera coordinate system to the nearest clipping plane.
  • \(z_{far}\) (far) => Distance from the camera coordinate system to the farthest clipping plane.

Observations:

  • The axis Z points the opposite direction to where the camera is looking at.
  • Any coordinate outside the range (\(x_{min}\), \(x_{max}\)), (\(y_{min}\), \(y_{max}\)) and (\(z_{far}\), \(z_{near}\)) will not be visible.
  • Those parameters are in the camera's coordinate system.

If a camera looking at the negative direction of Z axi is positioned at (x = 0, y = 0, z = 0) in world-coordinate origin. The ortographic projection for OpenGL's canonical view volume is the identity matrix.

  • \(x_{min} = -1\), \(x_{min} = +1\)
  • \(y_{min} = -1\), \(z_{min} = +1\)
  • \(z_{near} = +1\)
  • \(z_{far} = -1\)
\begin{equation} T_{\text{ortho}} = \text{ortho}(-1, +1, -1, +1, +1, -1) \\ = \begin{bmatrix} \frac{ 2 }{ (+1) - (-1) } & 0 & 0 & -\frac{ (+1) + (-1) }{ (+1) - (-1) } \\ 0 & \frac{ 2 }{ (+1) - (-1) } & 0 & -\frac{ (+1) + (-1) }{ (+1) - (-1) } \\ 0 & 0 & - \frac{ 2 }{ -(-1) + (+1) } & - \frac{ (-1) + (+1) }{ (-1) - (+1) } \\ 0 & 0 & 0 & 1 \end{bmatrix} \\ = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \end{equation}

Ortographic Projection for 2D drawing

The ortographic projection can be used 2D drawing by setting the coordinate Z of any vertex to zero, \(z_{near} = -1\) and \(z_{far} = +1\). Then, the ortographic projection becomes:

\begin{equation} T_{\text{ortho2D}} = \begin{bmatrix} \frac{ 2 }{ x_{max} - x_{min} } & 0 & 0 & -\frac{ x_{max} + x_{min} }{ x_{max} - x_{min} } \\ 0 & \frac{ 2 }{ y_{max} - y_{min} } & 0 & -\frac{ y_{max} + y_{min} }{ y_{max} - y_{min} } \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \end{equation}

If the transform is applied to any vertex with z = 0, the following outcome is achieved.

\begin{equation} \begin{bmatrix} x' \\ y' \\ z' \\ 1 \end{bmatrix} = T_{ortho2D} \begin{bmatrix} x \\ y \\ z = 0 \\ w = 1 \end{bmatrix} = \begin{bmatrix} \frac{ 2 }{ x_{max} - x_{min} } \cdot x - \frac{ x_{max} + x_{min} }{ x_{max} - x_{min} } \\ \frac{ 2 }{ y_{max} - y_{min} } \cdot y - \frac{ y_{max} + y_{min} }{ y_{min} - y_{min} } \\ 0 \\ 1 \\ \end{bmatrix} \end{equation}

Perspective projection matrix (with FOV - Field-of-View)

  • This matrix is generated by the following subroutines:
    • glm::perspective (FOV, aspect_ratio, zNear, zFar) => GLM library
    • glPerspective (FOV, aspect_ratio, zNear, zFar) => Old OpenGL.
\begin{equation} T_{\text{perspective}} = \begin{bmatrix} a_{11}& 0 & 0 & 0 \\ 0 & a_{22} & 0 & 0 \\ 0 & 0 & a_{33} & a_{34} \\ 0 & 0 & -1 & 0 \\ \end{bmatrix} \end{equation}

The matrix elements are:

\begin{eqnarray*} a_{11} &=& 1 / ( k \cdot \tan( \theta / 2 ) ) \\ a_{22} &=& 1 / \tan( \theta / 2 ) \\ a_{33} &=& \frac{ \text{zN} + \text{zF} }{ \text{zN} - \text{zF} } \\ a_{43} &=& \frac{ 2 \cdot \text{zN} \cdot \text{zF} }{ \text{zN} - \text{zF} } \\ \end{eqnarray*}

Where:

  • k - is the window aspect ration, the ratio between the window width and its height. So:
    • \(k = \text{width} / \text{height}\)
  • \(\theta\) - is the FOV (Field-Of-View angle)
  • \(zN\) (zNear) - Distance to near plane.
  • \(zF\) (zFar) - Distance to far plane.
  • Only camera-space coordinates within the range \(zN\) to \(zF\) will be displayed on the screen. Anything out of this range will be clipped.

This matrix transform coordinates in the following manner:

\begin{equation} \begin{bmatrix} x_{\text{clip}} \\ y_{\text{clip}} \\ z_{\text{clip}} \\ 1 \end{bmatrix} = T_{\text{projection}} \cdot \begin{bmatrix} x_{\text{camera}} \\ y_{\text{camera}} \\ z_{\text{camera}} \\ 1 \end{bmatrix} \end{equation}

Where:

  • The right-side (4 x 1) column vector (input) is the camera-space coordinate.
  • The left-side (4 x 1) column vector (ouput) is the clip-space coordinate. Any clip-space (NDC coordinates) coordinate out of the range -1.0 to 1.0 on each axis will not be displayed on the screen.

Perspective Projection Matrix (defined from View frustum) - (CSE167),

The perspective projection matrix provides a sense of depth, objects that are far from the camera will look like smaller and objects near the camera will look like smaller. Note: parallel lines are not preserved by this transform and also this transform is not affine.

  • This matrix can be computed by using subroutines:
    • glm::frustum (Xmin, Xmax, Ymin, Ymax, Zmin, Zmax)
    • glm::frustum (left, right, bottom, top, near, far)
\begin{equation} T_{\text{perspective}} = \begin{bmatrix} \frac{ 2 \cdot z_{near} }{ x_{max} - x_{min} } & 0 & \frac{ x_{max} + x_{min} }{ x_{max} - x_{min} } & 0 \\ 0 & \frac{ 2 \cdot z_{near} }{ y_{max} - y_{min} } & \frac{ y_{max} + y_{min} }{ y_{max} - y_{min} } & 0 \\ 0 & 0 & - \frac{ z_{far} + z_{near} }{ z_{far} - z_{near} } & - \frac{ z_{far} \cdot z_{near} }{ z_{far} - z_{near} } \\ 0 & 0 & -1 & 0 \end{bmatrix} \end{equation}

Aspect ratio and field of view (FOV) angle:

\begin{eqnarray*} \text{aspect ratio} &=& \frac{x_{max} - x_{min}}{ y_{min} - y_{max} } \\ \tan( \text{FOV} / 2) &=& \frac{ y_{max} }{ z_{min} } \end{eqnarray*}

Where:

  • \(x_{min}\) => (left) - Minimum X coordinate
    • Coordinate for the left clipping plane.
  • \(x_{max}\) => (right) - Maximum X coordinate
    • Coordinate for the right clipping plane.
  • \(y_{min}\) => (bottom) - Minimum Y coordinate
    • Coordinate for the bottom clipping plane.
  • \(y_{max}\) => (top) - Maximum Y coordinate
    • Coordinate for the top clipping plane.
  • \(z_{near}\) => (near)
    • Minimum Z coordinate - distance to the near depth clipping plane. The \(z_{near}\) value should never be zero, in this case, the \(z_{near}\) value should as close as possible to zero.
  • \(z_{far}\) => (far)
    • Maximum Z coordinate - distance to the far depth clippling plane.

Notes:

  • The camera is looking at the opposite direction of Z axis.
  • Any point in camera-space coordinate outside of the previous range will not be visible on the screen.
  • The parameters \(z_{far}\) and \(z_{near}\) must be positive.

Perspective and orthogonal transform matrix formula validation - in Julia Language

Running GLM math library in CERN's Root RPL:

root [0] .I .

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/string_cast.hpp>
#include <glm/gtx/io.hpp>

std::cout << std::setprecision(5);

// Note: GLM matrices are stored in Column-major order
void show_matrix(const char* label, glm::mat4 const& m){
    std::cout << "\n [MATRIX] " << label << " = " << '\n';
    std::cout << std::fixed << std::setprecision(5);
    for(size_t i = 0; i < 4; i++)
    {
        for(size_t j = 0; j < 4; j++)
        {
            std::cout << std::setw(10) << m[j][i];
        }
        std::cout << '\n';
    }
}


float xmin, xmax, ymin, ymax, zmin, zmax;

// --------- Test Case 1 --------------------------//
//
root [58] xmin = 4.0, xmax = 10.0, ymin = 0.1, ymax = 25.0, zmin = 8.0, zmax = 20.0;

root [59] glm::mat4 T = glm::frustum(xmin, xmax, ymin, ymax, zmin, zmax);

root [60] std::cout << " T = " << T << '\n'
 T =
[[    2.667,    0.000,    2.333,    0.000]
 [    0.000,    0.643,    1.008,    0.000]
 [    0.000,    0.000,   -2.333,  -26.667]
 [    0.000,    0.000,   -1.000,    0.000]]


// ------- Test Case 2 ------------------------------//

root [93] xmin = -20.0, xmax = 50.0, ymin = -10.0, ymax = 25.0, zmin = 0.1, zmax = 100.0;
root [94]
root [94] glm::mat4 T = glm::frustum(xmin, xmax, ymin, ymax, zmin, zmax)
(glm::mat4 &) @0x7f73c3d5d490
root [95]
root [95] show_matrix("T", T)

 [MATRIX] T =
   0.00286   0.00000   0.42857   0.00000
   0.00000   0.00571   0.42857   0.00000
   0.00000   0.00000  -1.00200  -0.20020
   0.00000   0.00000  -1.00000   0.00000
root [96]


// ------- Test Case 3 - Ortographics Matrix  -----------//

root [96] xmin = -20.0, xmax = 50.0, ymin = -10.0, ymax = 25.0, zmin = 0.1, zmax = 100.0;
root [97]
root [97] glm::mat4 T = glm::ortho(xmin, xmax, ymin, ymax, zmin, zmax)
(glm::mat4 &) @0x7f73c3d5d4d0
root [98]
root [98] show_matrix("T", T)

 [MATRIX] T =
   0.02857   0.00000   0.00000  -0.42857
   0.00000   0.05714   0.00000  -0.42857
   0.00000   0.00000  -0.02002  -1.00200
   0.00000   0.00000   0.00000   1.00000

Test in Julia programming language:

 function frustum(xmin, xmax, ymin, ymax, zmin, zmax)
      a11 = 2 * zmin / (xmax - xmin)
      a22 = 2 * zmin / (ymax - ymin)
      a13 = (xmax + xmin) / (xmax - xmin)
      a23 = (ymax + ymin) / (ymax - ymin)
      a33 = -(zmax + zmin) / (zmax - zmin)
      a34 = -2 * zmax * zmin / (zmax - zmin)
      T = [ a11 0 a13 0 ; 0 a22 a23 0; 0 0 a33 a34; 0 0 -1 0]
      return T
  end

  function ortho(xmin, xmax, ymin, ymax, zmin, zmax)
        a11 = 2 / (xmax - xmin)
        a22 = 2 / (ymax - ymin)
        a33 = -2 / (zmax - zmin)
        a14 = -(xmax + xmin ) / (xmax - xmin)
        a24 = -(ymin + ymax) / (ymax - ymin)
        a34 = -(zmax + zmin) / (zmax - zmin)
        T = [ a11 0 0 a14; 0 a22 0 a24 ; 0 0 a33 a34; 0 0 0 1]
        return T
  end


 ## --------- Test Case 1 --------------------------##

 julia> xmin = 4.0; xmax = 10.0; ymin = 0.1; ymax = 25.0; zmin = 8.0; zmax = 20.0;

 julia> frustum(xmin, xmax, ymin, ymax, zmin, zmax)
 4×4 Array{Float64,2}:
  2.66667  0.0       2.33333    0.0
  0.0      0.64257   1.00803    0.0
  0.0      0.0      -2.33333  -26.6667
  0.0      0.0      -1.0        0.0

 ## --------- Test Case 2 --------------------------##

julia> xmin = -20.0; xmax = 50.0; ymin = -10.0; ymax = 25.0; zmin = 0.1; zmax = 100.0;

julia> frustum(xmin, xmax, ymin, ymax, zmin, zmax)
4×4 Array{Float64,2}:
 0.00285714  0.0          0.428571   0.0
 0.0         0.00571429   0.428571   0.0
 0.0         0.0         -1.002     -0.2002
 0.0         0.0         -1.0        0.0

## -------- Test Case 3 - Ortographic Projection ---##

julia> xmin = -20.0; xmax = 50.0; ymin = -10.0; ymax = 25.0; zmin = 0.1; zmax = 100.0;

julia> ortho(xmin, xmax, ymin, ymax, zmin, zmax)
4×4 Array{Float64,2}:
 0.0285714  0.0         0.0      -0.428571
 0.0        0.0571429   0.0      -0.428571
 0.0        0.0        -0.02002  -1.002
 0.0        0.0         0.0       1.0

References

1.8.10 Concatenating Affine Transforms

Concatenating / Combinating of affine transforms

Multiple affine transforms can be combined or concatenated as a single affine transform, just by multiplying matrices. Considering the following sequence of 3D affine transforms, such as:

  1. scale(3, 3, 3) - scale by 3 on all axis.
  2. translate(x = 25, y = 10, z = 20) - translate to point (25, 30, 2)
  3. rotX(30) - rotate 30 degrees around X axis
  4. rotZ(54) - rotate by 54 degrees around Z axis.

Let V = [ x y z 1 ]' to be (4 x 1 - 4 rows and 1 column) a column vector containing the current vertice homogeneous coordinates.

Those transforms can be combined in the following way:

  • Node: the symbol (·) means multiplication.
// Where:
//  =>  scale(3, 3, 3) is the scaling affine transform matrix.
//  =>  Va is the vertex coordinate after applying rotZ(54)
//
Va = rotZ(54) · V

// Where:
// => translate(25, 10, 20) is the translation affine transfom matrix.
// => Vb is the result, vertex coodinate after the transform was applied.
Vb = rotX(30) · Va

Vc = translate(25, 10, 20) · Vb
Vd = scale(3, 3, 3) · Vc

// V' (V - prime) - overall transformation result (end result)
V' = Vd

By performing algebraics substitution, the overall transform result becomes:

STEP 1:
 Vb = rotX(30) · Va
 Vb = rotX(30) · ( rotZ(54) · Va )

STEP 2:
  Vc = translate(25, 10, 20) · Vb
  Vc = translate(25, 10, 20) ·  ( rotX(30) · ( rotZ(54) · Va ) )

STEP 3:
  Vd = scale(3, 3, 3) · Vc
  Vd = scale(3, 3, 3) · ( translate(25, 10, 20) ·  rotX(30) ·  rotZ(54) · Va )

STEP 4:  - End result
  V' = Vd
  V' = scale(3, 3, 3) · translate(25, 10, 20) ·  rotX(30) ·  rotZ(54) · Va 

The final vertex position can be stated as:

   V' = scale(3, 3, 3) · translate(25, 10, 20) ·  rotX(30) ·  rotZ(54) · Va 

OR:

   V' =  [ scale(3, 3, 3) · translate(25, 10, 20) ·  rotX(30) ·  rotZ(54) ] · V

All the sequence of transformations can be combined as a single transform, designated by T, which is the multiplication between all transforms in reverse order that they were applied. The last transform is multiplied first and first transform is multiplied last. In the same that matrix multiplication is not commutative, the operation result depends on the order that of matrices, transform combinations are not commutative, they also depends on the order that transforms were applied.

V' = T · V

Where:
   T =  scale(3, 3, 3) · translate(25, 10, 20) ·  rotX(30) ·  rotZ(54)

Multiple transforms can be combined as:

Tranform_combined = Transform[1] · Transform[2] · ... · Transform[N-1] · Transform[N]

Combining 2D canonical transforms in Sympy - Python CAS

Sympy - Python CAS (Computer Algebra System) can be used for debugging and testing affine transforms in a symbolic way.

Define 2D canonical affine transforms:

from sympy import *

x,  y  = symbols("x y")
dx, dy = symbols("dx dy")
sx, sy = symbols("sx sy")
t      = symbols("t")  # t represents the rotation angle theta (θ)


# Column vector representing - generic Vertex (aka point) in homogeneous coordinate
#
v = Matrix([x, y, 1])

>>> pprint(v)
⎡x⎤
⎢ ⎥
⎢y⎥
⎢ ⎥
⎣1⎦

# Translation of (dx, dy) coordinate transform
>>> Translate = Matrix([[1, 0, dx], [0, 1, dy], [0, 0, 1]])

>>> pprint(Translate)
⎡1  0  dx⎤
⎢        ⎥
⎢0  1  dy⎥
⎢        ⎥
⎣0  0  1 ⎦


# Scale affine transform
>>> Scale = Matrix([[sx, 0, 0], [0, sy, 0], [0, 0, 1]])

>>> pprint(Scale)
⎡sx  0   0⎤
⎢         ⎥
⎢0   sy  0⎥
⎢         ⎥
⎣0   0   1⎦


# Rotation around Z axis
>>> RotZ  = Matrix([[ cos(t), -sin(t), 0], [ sin(t), cos(t), 0], [0, 0, 1]])

>>> pprint( RotZ )
⎡cos(t)  -sin(t)  0⎤
⎢                  ⎥
⎢sin(t)  cos(t)   0⎥
⎢                  ⎥
⎣  0        0     1⎦

Apply single transforms to vertex v.

# Apply translation affine transform to vertex V
#-------------------------------------------
>>> pprint( Translate * v )
⎡dx + x⎤
⎢      ⎥
⎢dy + y⎥
⎢      ⎥
⎣  1   ⎦

# Apply scale to vertex v
#-------------------------------------------

>>> Scale * v
Matrix([
[sx*x],
[sy*y],
[   1]])

>>> pprint( Scale * v )
⎡sx⋅x⎤
⎢    ⎥
⎢sy⋅y⎥
⎢    ⎥
⎣ 1  ⎦

>>> pprint( (Scale * v).subs({sx: 10, sy: 10}) )
⎡10⋅x⎤
⎢    ⎥
⎢10⋅y⎥
⎢    ⎥
⎣ 1  ⎦


>>> pprint( (Scale * v).subs({sx: 10, sy: 20}) )
⎡10⋅x⎤
⎢    ⎥
⎢20⋅y⎥
⎢    ⎥
⎣ 1  ⎦


# Apply rotation to vertex v
#------------------------------

>>> v_rot = RotZ * v

>>> pprint( v_rot )
⎡x⋅cos(t) - y⋅sin(t)⎤
⎢                   ⎥
⎢x⋅sin(t) + y⋅cos(t)⎥
⎢                   ⎥
⎣         1         ⎦

>>> pprint( v_rot.subs(t, 0) )
⎡x⎤
⎢ ⎥
⎢y⎥
⎢ ⎥
⎣1⎦

# Rotate by 90 degrees (pi / 2 radians)
>>> pprint( v_rot.subs(t, pi / 2) )
⎡-y⎤
⎢  ⎥
⎢x ⎥
⎢  ⎥
⎣1 ⎦

# Rotate by 60 degreees (pi / 3)
>>> pprint( v_rot.subs(t, pi / 3) )
⎡x   √3⋅y⎤
⎢─ - ────⎥
⎢2    2  ⎥
⎢        ⎥
⎢√3⋅x   y⎥
⎢──── + ─⎥
⎢ 2     2⎥
⎢        ⎥
⎣   1    ⎦

Combine 2 tranforms in multiple orders and check the outcome.

# Combine transforms: 1st - translation; 2nd - scale.
#--------------------------------------------------

  >>> T = Scale * Translate

  >>> pprint(T)
  ⎡sx  0   dx⋅sx⎤
  ⎢             ⎥
  ⎢0   sy  dy⋅sy⎥
  ⎢             ⎥
  ⎣0   0     1  ⎦


  # Apply to vertex
  >>> pprint( T * v )
  ⎡dx⋅sx + sx⋅x⎤
  ⎢            ⎥
  ⎢dy⋅sy + sy⋅y⎥
  ⎢            ⎥
  ⎣     1      ⎦


# Combine transforms: 1st - scale; 2nd - translation.
#--------------------------------------------------

   >>> T = Translate * Scale

   >>> pprint( T )
   ⎡sx  0   dx⎤
   ⎢          ⎥
   ⎢0   sy  dy⎥
   ⎢          ⎥
   ⎣0   0   1 ⎦

   # Apply to vertex
   >>> pprint(T * v)
   ⎡dx + sx⋅x⎤
   ⎢         ⎥
   ⎢dy + sy⋅y⎥
   ⎢         ⎥
   ⎣    1    ⎦


# Combine transforms: 1st - rotation; 2nd - translation.
#--------------------------------------------------

   >>> T = Translate * RotZ

   >>> pprint( T )
   ⎡cos(t)  -sin(t)  dx⎤
   ⎢                   ⎥
   ⎢sin(t)  cos(t)   dy⎥
   ⎢                   ⎥
   ⎣  0        0     1 ⎦


   # Apply to vertex
   >>> pprint( T * v )
   ⎡dx + x⋅cos(t) - y⋅sin(t)⎤
   ⎢                        ⎥
   ⎢dy + x⋅sin(t) + y⋅cos(t)⎥
   ⎢                        ⎥
   ⎣           1            ⎦

# Combine transforms: 1st - translation ; 2nd - rotation
#------------------------------------------------------

   >>> T = RotZ * Translate

   >>> pprint( T )
   ⎡cos(t)  -sin(t)  dx⋅cos(t) - dy⋅sin(t)⎤
   ⎢                                      ⎥
   ⎢sin(t)  cos(t)   dx⋅sin(t) + dy⋅cos(t)⎥
   ⎢                                      ⎥
   ⎣  0        0               1          ⎦


   # Apply to vertex
   >>> pprint( T * v )
   ⎡dx⋅cos(t) - dy⋅sin(t) + x⋅cos(t) - y⋅sin(t)⎤
   ⎢                                           ⎥
   ⎢dx⋅sin(t) + dy⋅cos(t) + x⋅sin(t) + y⋅cos(t)⎥
   ⎢                                           ⎥
   ⎣                     1                     ⎦


Combine transforms in the following order: 1st - rotation; 2nd - scaling; 3rd - translation:

>>> T = Translate * Scale * RotZ

>>> pprint( T )
⎡sx⋅cos(t)  -sx⋅sin(t)  dx⎤
⎢                         ⎥
⎢sy⋅sin(t)  sy⋅cos(t)   dy⎥
⎢                         ⎥
⎣    0          0       1 ⎦

>>> pprint( T * v )
⎡dx + sx⋅x⋅cos(t) - sx⋅y⋅sin(t)⎤
⎢                              ⎥
⎢dy + sy⋅x⋅sin(t) + sy⋅y⋅cos(t)⎥
⎢                              ⎥
⎣              1               ⎦

1.8.11 3D Scene Coordinate Transformations

In a 3D scene, the following typical transformations happens to the vertex coordinates of some object to be rendered. The model matrix transform, \(T_m\), converts the object vertex coordinates from the local-space (a.k.a object-space) to world-space, then the view-matrix, \(T_v\), converts vertex world-space coordinates to the camera-space, finally the projection matrix transform, \(T_p\), turns the camera-space coordinates into clip-space coordinates (NDC - Normalized Device Coordinates) within the range -1.0 to 1.0 on each axis. Any NDC coordinate out of the range -1.0 to 1.0 will not be visible on the canvas, graphics window screen.

             +---------+              +---------+ Space      +---------------+  Clip-Space
 Local       |         |  World       |         |  Camera    |               |  (NDC - Coordinates)
Space        |   Model |  Space       |  View   |  Space     |  Projection   | 
   +------->>+  Matrix +------>>----->+ Matrix  +---------->>+   Matrix      +------->>>
 V           |         |   Vw         |         |   Vc       |               |  Vndc
             | Tm      |              |  Tv     |            |    Tp         |
             +---------+              +---------+            +---------------+


   Tm - (4x4) Model matrix transform 
   Tv - (4x4) View matrix transform 
   Tp - (4x4) Projection matrix transform. 

   V    - Vertex coordinate in local space 
   Vw   - Vertex coordinate in world-space
   Vc   - Vertex coordinate in camera-space coordinate (a.k.a eye-space)
   Vndc - Vertex coordinate in clip-space coordinate (NDC)
  • Transform from local-space coordinates to world-space coordinates
    • Where:
      • \(T_m\) - is the model-matrix, often comprised of rotation, scaling and translation concatened canonical transforms.
      • v - Vertex coordinate in local-space.
        • \(v = \begin{bmatrix} x & y & z & 1 \end{bmatrix}^T\)
      • \(v_w\) - Vertex coordinate in world-space
        • \(v_w = \begin{bmatrix} x_w & y_w & z_w & 1 \end{bmatrix}^T\)
\begin{equation} v_w = T_m \cdot v \end{equation}
  • Transform from world-space coordinates to camera-space coordinates (a.k.a eye space).
    • \(T_v\) - is the view matrix, computed using the eye vector, camera position; up vector - camera orientation; at point-vector, point which the camera is looking at.
\begin{equation} v_c = T_v \cdot v_w \end{equation}
  • Transform from camera-space to clip-space coordinates.
    • Where:
      • \(T_p\) - is the projection matrix which can be either the perspective projection matrix or ortographic projection matrix.
      • \(v_{\text{ndc}}\) - is the vertex NDC coordinate.
\begin{equation} v_{\text{ndc}} = T_p \cdot v_c \end{equation}
  • Overall scene transform from local-space to clip-space coordinates.
\begin{equation} v_{\text{ndc}} = \left( T_p \cdot T_v \cdot T_m \right) v \end{equation}
  • MVP transform - The overall transform is often designated as Model-View-Projection transform ( \(T_{mvp}\) ).
\begin{eqnarray*} v_{\text{ndc}} &=& T_{mvp} \cdot v \\ T_{mvp} &=& T_p \cdot T_v \cdot T_m \end{eqnarray*} \begin{equation} T_{\text{ModelViewProjection}} =T_{\text{Projection}} \cdot T_{\text{View}} \cdot T_{\text{Model}} \end{equation}
  • The Model-View transform is the product between the model and view transforms.
\begin{eqnarray*} T_{mv} &=& T_v \cdot T_m \\ T_{\text{ModelView}} &=& T_{\text{View}} \cdot T_{\text{Model}} \end{eqnarray*}
  • Normal transform matrix ( \(T_{normal}\) ) is used for illumination calculations, such as Phong illumination model, and is computed as the transpose of model-view matrix inverse. This transform maps vertex normal vector in the local-space coordinates to camera-space coordinates, also known as eye-space coordinates.
    • Where:
      • n - is the normal vector in local-space coordinates.
        • \(n = \begin{bmatrix} x_n & y_n & z_n & 0 \end{bmatrix}^T\)
      • \(n_c\) is the normal vector in camera-space (eye-space) coordinates.
\begin{equation} T_{normal} = \left[ T_{\text{ModelView}}^{-1} \right]^T = \left[ ( T_{\text{View}} \cdot T_{\text{Model}} )^{-1} \right]^T \end{equation} \begin{equation} T_{normal} = \left[ ( T_{\text{View}} )^{-1} \right]^T \cdot \left[ ( T_{\text{Model}} )^{-1} \right]^T \end{equation} \begin{equation} n_c = T_{normal} \cdot n \end{equation}

References

1.8.12 2D Scene Transforms

A 2D scene can be generated as particular case of a 3D scene by setting the vertex Z coordinate to always zero, replacing the view transform matrix with identity matrix and setting the projection matrix as the ortographic projection matrix. This approach only works if the graphics API default coordinate system is the same as the OpenGL one, where the Y and X axis are on the screen and the Z axis positive direction goes from the screen towards the viewer (computer user).

\begin{equation} v_{\text{ndc}} = \left( T_p \cdot T_v \cdot T_m \right) v \end{equation}

By following the previous approach for a 2D scene, the vertex transformation becomes:

\begin{equation} v_{\text{ndc}} = \left( T_{\text{ortographic}} \cdot T_{\text{model}} \right) \begin{bmatrix} x \\ y \\ 0 \\ 1 \end{bmatrix} \end{equation}

The model matrix is a combination of translation affine transforms with z set as zero, scaling affine transforms with Z set as zero and rotations around Z axis.

Some matrices that can be used for building the model matrix are:

  • 2D translation
\begin{equation} T_{\text{translation-2D}} = \begin{bmatrix} 1 & 0 & 0 & \Delta_x \\ 0 & 1 & 0 & \Delta_y \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}
  • 2D scaling
\begin{equation} T_{\text{scaling-2D}} = \begin{bmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}
  • 2D rotation (rotation around Z axis)
\begin{equation} T_{\text{rotation-2D}} = \begin{bmatrix} \cos{\theta} & -\sin{\theta} & 0 & 0 \\ \sin{\theta} & \cos{\theta} & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

1.8.13 Rotation Matrix and DCM - Direction Cosine Matrix

Let A and B be two reference frames in euclidean three dimensional space \(\mathbb{R}^3\) whose orthonormal basis vectors are \(\{\hat{a}_1,\hat{a}_2, \hat{a}_2 \}\) and \(\{\hat{b}_1, \hat{b}_2, \hat{b}_2 \}\) where \(\hat{a}_1\) corresponds to the X axis in the frame A, \(\hat{a}_2\) corresponds to the Y axis in the frame A, and \(\hat{a}_3\) corresponds to Z axis in frame A. The basis vectors satisfy the following properties, since they are orthonormal vectors, which means that they are unitary orthogonal (aka - also known as perpendicular) vectors.

a1 – x axis a2 – Y axis a3 – Z axis b3 – ‘Z axis a1 – x axis b1 – ‘x axis b2 – ‘Y axis Reference Frame A Reference Frame B C C \begin{eqnarray*} \| \hat{a}_i \| &=& 1 \quad \| \hat{b}_i \| = 1 \\ \hat{a}_i \cdot \hat{a}_j &=& 0 \quad \text{if } i \neq j \\ \hat{b}_i \cdot \hat{b}_j &=& 0 \quad \text{if } i \neq j \\ \end{eqnarray*}

A DCM - Direction Cosine Matrix, sometimes also called Direct Cosine Matrix, is a matrix that describes the attitude, also known as orientation, of a reference frame with respect to another reference frame. In this case \(\text{DCM}_{BA}\) is the matrix that describes the orientation of a rotated reference frame B with respect to reference frame A, in other words, the orientation of frame B relative to frame A, as shown below where the matrix \(\text{DCM}_{BA} = c_{ij}\) is expresses the basis vectors of frame B in terms of basis vectors of frame A.

\begin{eqnarray*} \hat{b}_1 &=& c_{11} \hat{a}_1 + c_{12} \hat{a}_2 + c_{13} \hat{a}_3 \\ \hat{b}_2 &=& c_{21} \hat{a}_1 + c_{22} \hat{a}_2 + c_{23} \hat{a}_3 \\ \hat{b}_2 &=& c_{31} \hat{a}_1 + c_{32} \hat{a}_2 + c_{33} \hat{a}_3 \\ \end{eqnarray*}

It is noticiable that \(c_{ij} = \hat{b}_i \cdot \hat{a}_j\), for instance

\begin{equation} c_{23} = \hat{b}_2 \cdot \hat{a}_3 = (c_{21} \hat{a}_1 + c_{22} \hat{a}_2 + c_{23} \hat{a}_3 ) \cdot \hat{a}_3 \end{equation} \begin{equation} c_{23} = c_{21} (\hat{a}_1 \cdot \hat{a}_3) + c_{22} (\hat{a}_2 \cdot \hat{a}_3) + c_{23} (\hat{a}_3 \cdot \hat{a}_3) \end{equation} \begin{equation} c_{23} = c_{23} \end{equation}

By expressing the dot product \(\hat{b}_i \cdot \hat{a}_j\) in terms of the cosine between vector angles, it is possible to find the next relation.

\begin{equation} c_{ij} = \hat{b}_i \cdot \hat{a}_j = \| \hat{b}_i \| \| \hat{a}_j \| \cos(\hat{b}_i, \hat{a}_j) \end{equation} \begin{equation} \boxed{ c_{ij} = \hat{b}_i \cdot \hat{a}_j = \cos(\hat{b}_i, \hat{a}_j) = \cos \theta_{ij} } \end{equation}

Where:

  • \(\cos(\hat{b}_i, \hat{a}_j)\) is the cosine of the angle between the vectors \(\hat{b}_i\) and \(\hat{a}_j)\).
  • \(\theta_{ij}\) is the angle between vectros \(\hat{b}_i\) and \(\hat{a}_j\) .

So, the previous expression in matrix form becomes:

\begin{equation} \begin{pmatrix} \hat{b}_1 \\ \hat{b}_2 \\ \hat{b}_3 \end{pmatrix} = \begin{pmatrix} c_{11} & c_{12} & c_{13} \\ c_{21} & c_{22} & c_{23} \\ c_{31} & c_{32} & c_{33} \\ \end{pmatrix} \begin{pmatrix} \hat{a}_1 \\ \hat{a}_2 \\ \hat{a}_3 \end{pmatrix} \end{equation}

So, it can be found that the matrix \(\text{DCM}_{BA}\) is given by the following expression where the first formula expresses the matrix in terms of coefficients \(c_{ij}\), the second formula expresses the matrix in terms of the dot product between reference frames basis vectors; the third states is defined in terms of the cosine between basis vectors of reference frames A and B.

\begin{equation} \text{DCM}_{BA} = \begin{pmatrix} c_{11} & c_{12} & c_{13} \\ c_{21} & c_{22} & c_{23} \\ c_{31} & c_{32} & c_{33} \\ \end{pmatrix} \end{equation} \begin{equation} \text{DCM}_{BA} = \begin{pmatrix} \\ \hat{b}_1 \cdot \hat{a}_1 & \hat{b}_1 \cdot \hat{a}_2 & \hat{b}_1 \cdot \hat{a}_3 \\ \hat{b}_2 \cdot \hat{a}_1 & \hat{b}_2 \cdot \hat{a}_2 & \hat{b}_2 \cdot \hat{a}_3 \\ \hat{b}_3 \cdot \hat{a}_1 & \hat{b}_3 \cdot \hat{a}_2 & \hat{b}_3 \cdot \hat{a}_3 \end{pmatrix} = \begin{pmatrix} ^A\hat{b}_1^T \\ ^A\hat{b}_2^T \\ ^A\hat{b}_3^T \end{pmatrix} \end{equation} \begin{equation} \text{DCM}_{BA} = \begin{pmatrix} \\ \cos( \hat{b}_1, \hat{a}_1) & \cos(\hat{b}_1, \hat{a}_2) & \cos(\hat{b}_1, \hat{a}_3) \\ \cos( \hat{b}_2, \hat{a}_1) & \cos(\hat{b}_2, \hat{a}_2) & \cos(\hat{b}_2, \hat{a}_3) \\ \cos( \hat{b}_3, \hat{a}_1) & \cos(\hat{b}_3, \hat{a}_2) & \cos(\hat{b}_3, \hat{a}_3) \end{pmatrix} \end{equation}

The \(\text{DCM}_{BA}\) could also be expressed in more concise way in this form.

\begin{equation} \boxed{ \text{DCM}_{BA} = \begin{pmatrix} ^A\hat{b}_1^T \\ ^A\hat{b}_2^T \\ ^A\hat{b}_3^T \end{pmatrix} } \end{equation}

Where:

  • \(^A\hat{b}_1^T\) is the transpose of frame A coordinates of vector \(\hat{b}_1\). In other words, it is projection of \(\hat{b}_1\) onto all basis vectors of reference frame A. Note: all vectors are assumed to be column vectors by default.
  • \(^A\hat{b}_2^T\) is the transpose of frame A coordinates of vector \(\hat{b}_2\).
  • \(^A\hat{b}_3^T\) is the transpose of frame A coordinates of vector \(\hat{b}_3\).

Coordinate Transformation between frames - A point-vector \(\boldsymbol{r}\) representing a position in the space can be expressed in both frames A and B as linear combination of basis vectors.

\begin{equation} \boldsymbol{r} = r_1 \hat{a}_1 + r_2 \hat{a}_2 + r_3 \hat{a}_3 = r_1' \hat{b}_1 + r_2' \hat{b}_2 + r_3' \hat{b}_3 \end{equation}

The coordinates of the vector \(\boldsymbol{r}\) in referece frame B can be found by taking the dot product between the vector and each basis vectors of frame B.

\begin{eqnarray*} \\ r_1' &=& \hat{b}_1 \cdot \boldsymbol{r} = \hat{b}_1 \cdot (r_1 \hat{a}_1 + r_2 \hat{a}_2 + r_3 \hat{a}_3 ) \\ r_2' &=& \hat{b}_2 \cdot \boldsymbol{r} = \hat{b}_2 \cdot (r_1 \hat{a}_1 + r_2 \hat{a}_2 + r_3 \hat{a}_3 ) \\ r_3' &=& \hat{b}_3 \cdot \boldsymbol{r} = \hat{b}_3 \cdot (r_1 \hat{a}_1 + r_2 \hat{a}_2 + r_3 \hat{a}_3 ) \end{eqnarray*}

So, the it can be found that the previous expression can be reduced to the following expression and the matrix \(\text{DCM}_{BA}\) is the rotation matrix \({}^{B}_{A}R = \text{DCM}_{BA}\) that maps coordinates from frame A to frame B.

\begin{equation} {}^B\boldsymbol{r} = \text{DCM}_{BA} {}^A\boldsymbol{r} \end{equation}

Where:

  • \(^B\boldsymbol{r}\) - is the coordinate of position vector in frame B (rotated frame)
  • \(^A\boldsymbol{r}\) - is the coordinate of position vector in frame A.

Opposite Transformation - It is often desirable to find the rotation matrix that maps coordinates from roted frame B to the frame A. It can be acomplished by taking the dot product between the previous position vector \(\boldsymbol{r}\) and each basis vector of frame A.

\begin{eqnarray*} \\ r_1 &=& \hat{a}_1 \cdot \boldsymbol{r} = \hat{a}_1 \cdot (r_1' \hat{b}_1 + r_2' \hat{b}_2 + r_3' \hat{b}_3 ) \\ r_2 &=& \hat{a}_2 \cdot \boldsymbol{r} = \hat{a}_2 \cdot (r_1' \hat{b}_1 + r_2' \hat{b}_2 + r_3' \hat{b}_3 ) \\ r_3 &=& \hat{a}_3 \cdot \boldsymbol{r} = \hat{a}_3 \cdot (r_1' \hat{b}_1 + r_2' \hat{b}_2 + r_3' \hat{b}_3 ) \end{eqnarray*}

The previous expression can be simplified as:

\begin{equation} \begin{pmatrix} r_1 \\ r_2 \\ r_3 \end{pmatrix} = \begin{pmatrix} \\ \hat{a}_1 \cdot \hat{b}_1 & \hat{a}_1 \cdot \hat{b}_2 & \hat{a}_1 \cdot \hat{b}_3 \\ \hat{a}_2 \cdot \hat{b}_1 & \hat{a}_2 \cdot \hat{b}_2 & \hat{a}_2 \cdot \hat{b}_3 \\ \hat{a}_3 \cdot \hat{b}_1 & \hat{a}_3 \cdot \hat{b}_2 & \hat{a}_3 \cdot \hat{b}_3 \end{pmatrix} \begin{pmatrix} r_1' \\ r_2' \\ r_3' \end{pmatrix} \end{equation}

It is the same as a multiplication by the transpose of the direct cosine matrix.

\begin{equation} {}^A\boldsymbol{r} = (\text{DCM}_{BA})^T {}^A\boldsymbol{r} \end{equation}

So, it is possible to find that the rotation matrix that maps coordinates from frame B to frame A is the tranpose of the DCM - direct cosine matrix \(\text{DCM}_{BA}\).

\begin{equation} \boxed{ {}^{A}_{B}R = \text{DCM}_{BA}^T = \text{DCM}_{AB} } \end{equation}

The rotation matrix \({}^{A}_{B}R\) can be quick determined using the following formula where each column of the rotation matrix is a basis vector of the rotated frame B expressed in terms of basis vectors of frame A. For instance, the first column of this rotation matrix is a column vector that contains the coordinates of basis vector \(\hat{b}_1\) in reference frame A, which is the same as the projection of this basis vector onto each basis vectors of frame A.

\begin{equation} \boxed{ {}^{A}_{B}R = \begin{pmatrix} ^A\hat{b}_1 & ^A\hat{b}_2 & ^A\hat{b}_3 \end{pmatrix} } \end{equation}

Example - Rotation around Z axis

Given two frames A, whose orthornmal basis vectors are \({a_1, a_2, a_3}\), and B whose basis vectors are \({b_1, b_2, b_3}\). The reference frame A is static (innertial reference frame) and the reference frame B is the frame A rotated counterclockwise around the Z axis by an angle \(\theta\).

a3, b3 z axis (orthogonal to screen)This axis goes outside of screen plane a1 – X axis b1 – X axis a2 – Y axis b2 – ‘Y axis θ sin θ 1 cos θ θ 1 sin θ b2 b1 Rotation around Z axis θ cos θ b1 b2 – ‘Y axis θ 1 sin θ b2 Projection of b2 onto frame A Projection of b1 onto frame A cos θ sin θ cos θ

APPROACH 1 => Finding the Direction Cosine Matrix \(\text{DCM}_{BA}\) by using dot product or angle between vectors

\begin{equation} \text{DCM}_{BA} = \begin{pmatrix} \\ \hat{b}_1 \cdot \hat{a}_1 & \hat{b}_1 \cdot \hat{a}_2 & \hat{b}_1 \cdot \hat{a}_3 \\ \hat{b}_2 \cdot \hat{a}_1 & \hat{b}_2 \cdot \hat{a}_2 & \hat{b}_2 \cdot \hat{a}_3 \\ \hat{b}_3 \cdot \hat{a}_1 & \hat{b}_3 \cdot \hat{a}_2 & \hat{b}_3 \cdot \hat{a}_3 \end{pmatrix} \end{equation}

Find the the dot products between basis vectors.

  • \(\hat{b}_1 \cdot \hat{a}_1 = \cos( \hat{b}_1, \hat{a}_1) = \cos \theta\)
  • \(\hat{b}_1 \cdot \hat{a}_2 = \cos( \hat{b}_1, \hat{a}_2) = \cos(90 - \theta) = \sin \theta\)
  • \(\hat{b}_1 \cdot \hat{a}_4 = \cos( \hat{b}_1, \hat{a}_3) = \cos(90) = 0\)
  • \(\hat{b}_2 \cdot \hat{a}_1 = \cos( \hat{b}_2, \hat{a}_1) = \cos (90 + \theta) = - \sin \theta\)
  • \(\hat{b}_2 \cdot \hat{a}_2 = \cos( \hat{b}_2, \hat{a}_2) = \cos \theta\)
  • \(\hat{b}_2 \cdot \hat{a}_3 = \cos( \hat{b}_2, \hat{a}_3) = \cos 90 = 0\)
  • \(\hat{b}_3 \cdot \hat{a}_1 = \cos( \hat{b}_3, \hat{a}_1) = \cos 90 = 0\)
  • \(\hat{b}_3 \cdot \hat{a}_2 = \cos( \hat{b}_3, \hat{a}_2) = \cos 90 = 0\)
  • \(\hat{b}_3 \cdot \hat{a}_3 = \cos( \hat{b}_3, \hat{a}_3) = \cos 0 = 1\)

So, the DCM matrix becomes:

\begin{equation} \text{DCM}_{BA} = \begin{bmatrix} \cos \theta & \sin \theta & 0 \\ -\sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

The rotation matrix \({}^{A}_{B}R\) that maps vectors from frame B, rotated reference frame, to frame A, static reference frame, can be found by transposing the previous matrix

\begin{equation} {}^{A}_{B}R = \text{DCM}_{BA}^T = \begin{bmatrix} \cos \theta & -\sin \theta & 0 \\ \sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

APPROACH 2 => Finding the Direction Cosine Matrix \(\text{DCM}_{BA}\) by projecting basis vectors.

STEP 1 => Write each basis vector of frame B in terms of basis vectors of frame A, in other words write the frame A coordinates of each basis vector of B by projecting each of them onto frame A basis vectors.

\begin{equation} ^A\hat{b}_1 = \begin{bmatrix} \cos \theta \\ \sin \theta \\ 0 \end{bmatrix} \quad ^A\hat{b}_2 = \begin{bmatrix} -\sin \theta \\ \cos \theta \\ 0 \end{bmatrix} \quad ^A\hat{b}_3 = \begin{bmatrix} 0 \\ 0 \\ 1 \end{bmatrix} \end{equation}

STEP 2 => Build the DCM matrix using the previous vectors.

\begin{equation} {}^{B}_{A}R = \text{DCM}_{BA} = \begin{pmatrix} ^A\hat{b}_1^T \\ ^A\hat{b}_2^T \\ ^A\hat{b}_3^T \end{pmatrix} = \begin{bmatrix} \cos \theta & \sin \theta & 0 \\ -\sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

The rotation matrix that maps vectors from frame B to frame A or \({}^{A}_{B}R\) can be found by tranposing the previous matrix.

\begin{equation} {}^{A}_{B}R = \text{DCM}_{BA}^T = \begin{bmatrix} \cos \theta & -\sin \theta & 0 \\ \sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

The rotation matrix \({}^{A}_{B}R\) could also be found in this way.

\begin{equation} {}^{A}_{B}R = \begin{pmatrix} ^A\hat{b}_1 & ^A\hat{b}_2 & ^A\hat{b}_3 \end{pmatrix} = \begin{bmatrix} \cos \theta & -\sin \theta & 0 \\ \sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

References:

1.8.14 Default Coordinate Systems

In order to properly use a graphics application programming interface, one must know the default coordinate system and conventions defined by the graphics API. This convention includes: the coordinate system origin, which may be on the upper left corner of drawing window or lower left corner of drawing window; the default direction of axis and whether the coordinate system is RHS (Right-Handed System) or LHS (Left-Handed System).

OpenGL Default Coordinate System

OpenGL API uses a default coordinate system with the origin place at the screen center, X axis as the horizontal axis, Y axis as the vertical axis and Z axis going from the screen center towards the user. This default coordinate system NDC is normalized, each dimension X, Y and Z has the range from -1.0 to +1.0. Any vertex with any dimension out of this range is not displayed on the screen.

In this coordinate system, the coordinate of points: point A at the OpenGL window top right corner is (x = 1, y = 1); the coordinate of point B is (x = 1,y= -1); the coordinate of the point C is (x = -1, y = -1).

default-coordinate-system1.png

Figure 3: OpenGL default coordinate system (NDC - Normalized Device Coordinate)

OpenGL Coordinate System X Math-textbook Coordinate System

OpenGL default coordinate system should not be confused with the coordinate system commonly found in math and physics textbooks, where Z axis is the vertical axis, Y axis is the horizontal axis and the X axis is going from the screen twoards the screen observer (out of screen). The advantage of OpenGL coordinate system over the coordinate system used by textbooks is that 2D drawing requires just ignoring the Z axis, setting any Z coordinate to zero. If one does not like the OpenGL default coordinate system, it is possible to obtain the math textbook coordinate system just by applying affine rotation transform the OpenGL default coordinate system.

default-coordinate-system2.png

Figure 4: OpenGL coordinate system versus math or physics coordinate system.

The transform matrix that maps coordinates from the math and physics style coordinate system (M) to the OpenGL coordinate (C) can be obtained by:

\begin{equation} T_{M -> C} = \text{Rot}_x(-90) \cdot \text{Rot}_z(-90) \end{equation} \begin{equation} T_{M -> C} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & -1 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 0 & 1 & 0 & 0 \\ -1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \end{equation} \begin{equation} T_{M -> C} = \begin{bmatrix} 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \end{equation} \begin{equation} \underset{ \text{OpenGL Coord.} }{ \begin{bmatrix} x_o \\ y_o \\ z_o \\ 1 \end{bmatrix} } = \begin{bmatrix} 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \underset{ \text{Math Coord.} }{ \begin{bmatrix} x_m \\ y_m \\ z_m \\ 1 \end{bmatrix} } \end{equation}

Where:

  • \(Rot_x\) - Is the X axis rotation matrix.
  • \(Rot_z\) - Is the Z axis rotation matrix.
  • \(T_{M -> C}\) - Is the affine matrix transform that maps coordinates from math-and-physics coordinate system to OpenGL coordinate system.
  • M - designates the coordinate system similar to the math-and-physics textbooks as in the right side of the previous picture.
  • C - designates the openGL coordinate system.

Coordinates from math textbook style coordinate systems can be mapped to OpenGL by using the previous affine transform matrix.

\begin{equation} \begin{bmatrix} x_c \\ y_c \\ z_c \\ 1 \end{bmatrix} = T_{M -> C} \begin{bmatrix} x_m \\ y_m \\ z_m \\ 1 \end{bmatrix} \end{equation}

With the previous equation it is possible to verify that the Y axis, vector (0, 1, 0) from the M coordinate system corresponds to the axis X (1, 0, 0) in the openGL (C) coordinate system.

\begin{equation} \begin{bmatrix} 1 \\ 0 \\ 0 \\ 1 \end{bmatrix} = \begin{bmatrix} 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 0 \\ 1 \\ 0 \\ 1 \end{bmatrix} \end{equation}

The same procedure can be applied to X axis in (M) coordinate system, which is mapped to Z axis in OpenGL coordinate system and to Z axis from (M) coordinate system which is mapped to Y axis in OpenGL coordinate system.

Alternative Approach - using basis vectors

The rotation matrix that maps from math and physics reference frame M to Opengl reference frame, designated as C, is a matrix whose columns are the basis vectors (unit vectors) of each axis of reference frame M expressed in coordinates of reference frame C.

\begin{equation} ^C\hat{X}_M = \begin{pmatrix} 0 \\ 0 \\ 1 \end{pmatrix} \quad ^C\hat{Y}_M = \begin{pmatrix} 1 \\ 0 \\ 0 \end{pmatrix} \quad ^C\hat{Z}_M = \begin{pmatrix} 0 \\ 1 \\ 0 \end{pmatrix} \end{equation}

Where:

  • \(^C\hat{X}_M\) - is the X axis basis vector of frame M expressed in coordinates of reference frame C (OpenGL reference frame).
  • \(^C\hat{Y}_M\) - is the Y axis basis vector of frame M expressed in coordinates of reference frame C (OpenGL reference frame).
  • \(^C\hat{Z}_M\) - is the Z axis basis vector of frame M expressed in coordinates of reference frame C (OpenGL reference frame).

So, the rotation matrix that maps from frame M to frame C becomes:

\begin{equation} \boxed{ {}^{C}_{M}R = \begin{pmatrix} ^C\hat{X}_M & ^C\hat{Y}_M & ^C\hat{Z}_M \end{pmatrix} = \begin{pmatrix} 0 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \\ \end{pmatrix} } \end{equation}

The homogenous 4 x 4 by matrix that maps from M frame M to frame C (OpengL frame) can be obtained as follow:

\begin{equation} \boxed{ {}^{C}_{M}T = \begin{pmatrix} {}^{C}_{M}R & \boldsymbol{0}_{3 \times 1} \\ \boldsymbol{0}_{1 \times 3} & 1 \end{pmatrix} = \begin{pmatrix} 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} } \end{equation}

Coordinate System positions from other computer graphics APIS

The following diagram shows the possible default coordinate systems positions from many computer graphics APIS:

default-coordinate-system3.png

Figure 5: Default coordinate systems positions

  • (C) => Center of display screen
    • Default position of OpenGL NDC coordinate system (clip-space).
  • (B) => Lower-left corner of display screen
    • Default position of OpenGL screen coordinate system
  • (U) => Upper-left corner of display screen
    • => The Y axis is inverted, it is positive in the downward direction instead of upward direction.
    • Default position of most 2D graphics APIs including: Html5 canvas; SVG; Java AWT 2D.

Transform matrix, \(T_{B \rightarrow U}\), that maps coordinates from lower-left corner (B) coordinate system to upper-left-corner coordinate system.

\begin{equation} T_{B \rightarrow U} = \begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & h \\ 0 & 0 & 1 \end{bmatrix} \end{equation}

Transform matrix, \(T_{C \rightarrow U}\), that maps coordinates from (C) window-center coordinate system to upper-left corner (U) coordinate system. This transform is useful for drawing with origin on screen center when using gaphics APIs which the default coordinate system is located at the upper-left corner.

\begin{equation} T_{C \rightarrow U} = \begin{bmatrix} 1 & 0 & w/2 \\ 0 & -1 & h/2 \\ 0 & 0 & 1 \end{bmatrix} \end{equation} \begin{equation} V_U = T_{C \rightarrow U} \cdot V_C \end{equation} \begin{equation} \begin{bmatrix} x_u \\ y_u \\ 1 \end{bmatrix} = T_{C \rightarrow U} \begin{bmatrix} x_c \\ y_c \\ 1 \end{bmatrix} = \begin{bmatrix} x_c + w/2 \\ -y_c + h/2 \\ 1 \end{bmatrix} \end{equation}

Transform matrix, \(T_{B \rightarrow C}\), that maps coordinates from lower-left corner (B) to screen-center origin, OpenGL normalized coordinate system (C).

\begin{equation} T_{B \rightarrow C} = \begin{bmatrix} 2/w & 0 & -1 \\ 0 & 2/h & -1 \\ 0 & 0 & 1 \end{bmatrix} \end{equation}

The matrix transform, \(T_{C \rightarrow B}\), that maps from coordinate system C to coordinate system B can be computed as the inverse of \(T_{B \rightarrow C}\).

\begin{equation} T_{C \rightarrow B} = T_{B \rightarrow C}^{-1} = \begin{bmatrix} w/2 & 0 & w/2 \\ 0 & h/2 & h/2 \\ 0 & 0 & 1 \end{bmatrix} \end{equation}

1.8.15 Column-Major X Row-major Matrices

OpenGL ES (embedded version of OpenGL) and WebGL APIs no longer support matrices in row-major order memory layout, where elements of rows are contiguous in memory. Instead, those APIs only support matrices in column-major memory layout, where elements of columns are contiguous in memory.

So, in modern OpenGL, the following API no longer works with boolean value 'true' that indicates that the matrix passed as a argument is in row-major oder format:

// WebGL API - (NO LONGER POSSIBLE =>> ERROR!) 
gl.uniformMatrix4fv(u_model, true, matrix_in_row_major_order); 

 // OpenGL ES - API (NO LONGER POSSIBLE!!!! =>> ERROR!!!)
 // Where u_model is a shader uniform variable 
 glUniformMatrix4fv(u_model, 1, GL_TRUE,  matrix_in_row_major_order );

Now, only this following usage works:

// WebGL API 
gl.uniformMatrix4fv(u_model, false, matrix_in_column_major_order); 

 // OpenGL ES - API 
 // Where u_model is a shader uniform variable 
 glUniformMatrix4fv(u_model, 1, GL_FALSE,  matrix_in_column_major_order);

Further reading:

Row-major matrices

Let the array T of type float[16], containing 16 32 bits floating point numbers, be a comumn-major matrix represeting some affine transform or projection transform.

T: float[16] = [ T[0], T[1], T[2], T[3], T[4], T[5], T[6], T[7], T[8], T[9], T[11], T[12], T[13], T[14], T[15] ]

The array T float[16], represents the following matrix in row-major order:

     /                           \
     |  T[00] T[01] T[02]  T[03]  |   
     |  T[04] T[05] T[06]  T[07]  | 
 T = |  T[08] T[09] T[10]  T[11]  |
     |  T[12] T[13] T[14]  T[15]  |
     \                            /

     /                             \       
     |  A(1,1) A(1,2) A(1,3) A(1,4) |  
     |  A(2,1) A(2,2) A(2,3) A(2,4) |
 T = |  A(3,1) A(3,2) A(3,3) A(3,4) |
     |  A(4,1) A(4,2) A(4,3) A(4,4) |
     \                             /

It follows that: 

  T[00] = A(1,1) ; T[01] = A(1,2) ; T[02] = A(1,3) ; T[03] = A(1,4)
  T[04] = A(2,1) ; T[05] = A(2,2) ; T[06] = A(2,3) ; T[07] = A(2,4)
  T[08] = A(3,1) ; T[09] = A(3,2) ; T[10] = A(3,3) ; T[11] = A(3,4)
  T[12] = A(4,1) ; T[13] = A(4,2) ; T[14] = A(4,3) ; T[15] = A(4,4)


 T: float[16] = [  A(1,1), A(1,2), A(1,3), A(1,4)
                 , A(2,1), A(2,2), A(2,3), A(2,4)
                 , A(3,1), A(3,2), A(3,3), A(3,4)
                 , A(4,1), A(4,2), A(4,3), A(4,4)
               ];

Column-major matrices

Let the array T of type float[16], containing 16 32 bits floating point numbers, be a comumn-major matrix represeting some affine transform or projection transform.

T: float[16] = [ T[0], T[1], T[2], T[3], T[4], T[5], T[6], T[7], T[8], T[9], T[11], T[12], T[13], T[14], T[15] ]

The array T represents the following column-major matrix:

             Matrix in Column-Major Order  
       _                                        _ 
      /                                          \
     +                                            +
T  = | COLUMN[0], COLUMN[1], COLUMN[2], COLUMN[3] |
     +                                            +
      \_                                        _/  

    /                           \
    |  T[00] T[04] T[08]  T[12]  |
    |  T[01] T[05] T[09]  T[13]  |
T = |  T[02] T[06] T[10]  T[14]  |
    |  T[03] T[07] T[11]  T[15]  |
    \                            /

  Where:

                   | T[0] |               | T[4] |              | T[8]  |              | T[12] |
                   | T[1] |               | T[5] |              | T[9]  |              | T[13] |
       COLUMN[0] = | T[2] |   COLUMN[1] = | T[6] |  COLUMN[2] = | T[10] |  COLUMN[3] = | T[14] |
                   | T[3] |               | T[7] |              | T[11] |              | T[15] |

By applying the previous idea to this matrix, it is possible to find:

    /                             \       
    |  A(1,1) A(1,2) A(1,3) A(1,4) |  
    |  A(2,1) A(2,2) A(2,3) A(2,4) |
T = |  A(3,1) A(3,2) A(3,3) A(3,4) |
    |  A(4,1) A(4,2) A(4,3) A(4,4) |
    \                             /

T[00] = A(1,1) ; T[01] = A(2,1) ; T[02] = A(3,1) ; T[03] = A(4,1)
T[04] = A(1,2) ; T[05] = A(2,2) ; T[06] = A(3,2) ; T[07] = A(4,2)
T[08] = A(1,3) ; T[09] = A(2,3) ; T[10] = A(3,3) ; T[11] = A(4,3)
T[12] = A(1,4) ; T[13] = A(2,4) ; T[14] = A(3,4) ; T[15] = A(4,4)


T: float[16] = [  A(1,1), A(2,1), A(3,1), A(4,1)
                , A(1,2), A(2,2), A(3,2), A(4,2)
                , A(1,3), A(2,3), A(3,3), A(4,3)
                , A(1,3), A(2,4), A(3,4), A(4,4)
              ];

The array T can be converted to a matrix by using the next formula:

//----------------------------------//
// Matrix using 1 as initial index  //
//----------------------------------//
// Line 1 
A(1, 1) = T[0]  
A(1, 2) = T[4]  
A(1, 3) = T[8] 
A(1, 4) = T[12] 
// Line 2 
A(2, 1) = T[1]   
A(2, 2) = T[5] 
A(2, 3) = T[9] 
A(2, 4) = T[13]
// Line 3 
A(3, 1) = T[2]
A(3, 2) = T[6]
A(3, 3) = T[10]
A(3, 4) = T[14]
// Line 4 
A(4, 1) = T[3]
A(4, 2) = T[7]
A(4, 3) = T[11]
A(4, 4) = T[15]

//----------------------------------//
// Matrix using 0 as initial index  //
//----------------------------------//
// Line 1 
A(0, 0) = T[0]  
A(0, 1) = T[4]  
A(0, 2) = T[8] 
A(0, 3) = T[12] 
// Line 2 
A(1, 0) = T[1]   
A(1, 1) = T[5] 
A(1, 2) = T[9] 
A(1, 3) = T[13]
// Line 3 
A(2, 0) = T[2]
A(2, 1) = T[6]
A(2, 2) = T[10]
A(2, 3) = T[14]
// Line 4 
A(3, 0) = T[3]
A(3, 1) = T[7]
A(3, 2) = T[11]
A(3, 3) = T[15]

Example: Given the translation matrix T

     | 1  0  0  tx |
     | 0  1  0  ty |
 T = | 0  0  1  tz |
     | 0  0  0   1 |

 T[00] = A(1,1) = 1 ; T[01] = A(2,1) = 0 ; T[02] = A(3,1) = 0 ; T[03] = A(4,1) = 0 (column 1)
 T[04] = A(1,2) = 0 ; T[05] = A(2,2) = 1 ; T[06] = A(3,2) = 0 ; T[07] = A(4,2) = 0 (column 2)
 T[08] = A(1,3) = 0 ; T[09] = A(2,3) = 0 ; T[10] = A(3,3) = 1 ; T[11] = A(4,3) = 0 (column 3)
 T[12] = A(1,4) = tx; T[13] = A(2,4) = ty; T[14] = A(3,4) = tz; T[15] = A(4,4) = 1 (column 4)

Hence, 

 T: float[16] = {  1,  0,  0,  0
                 , 0,  1,  0,  0
                 , 0,  0,  1,  0
                 , tx, ty, tz, 1 }; 

 T: float[16] = { 1,  0,  0,  0, 0,  1,  0,  0, 0,  0,  1,  0, tx, ty, tz, 1 }

 gl.uniformMatrix4fv(u_model, false, T);

This matrix in column-major order is represented as:

                                | 1   0   0  0 |
                                | 0   1   0  0 |
T             =  TRANSPOSE(T) = | 0   0   1  0 |
 column-major                   | tx ty  tz  1 |

1.8.16 Quaternions

Quaternions are a generalization of complex numbers, that was proposed and formulated by the irish mathematician and physicist William Rowan Hamilton, in 1843. This mathematical construct provides a convenient notation for expressing rotation and has a broad range of applications, including computer graphics, computer vision, robotics, orbital mechanics, aerospace and flight dynamics.

A quaternion q is defined as:

\begin{equation} q = w + x \cdot i + y \cdot j + z \cdot k \end{equation}
  • Where:
    • w, x, y, z are real numbers.
    • w => is the quaternion's real (scalar) component.
    • [x, y, z] => is the quaternion's vector componet
    • i, j, k are unit vectors (basis elements) that satisfies a set of properties.

The quaternion q can be represented in the following manner:

\begin{align*} q &= [w, x, y, z] \quad & \text{Tuple representation} \\ q &= (u, \vec{v}) \quad & \text{Scalar/vector representation} \\ \end{align*}
  • Where:
    • \(\vec{v} = [x, y, z]\) is the quaternion vector component.

The quaternion unit vectors or basis elements satifies the following properties:

\begin{equation} i^2 = j^2 = k^2 = i \cdot j \cdot k = -1 \end{equation}

And also:

\begin{align*} i \cdot j = k & \quad & j \cdot i = -k \\ j \cdot k = i & \quad & k \cdot j = -i \\ k \cdot i = j & \quad & i \cdot k = -j \\ \end{align*}

Types of Quaternion

  • Real quaternion

Real quaternion, is a quaterion which all elements of vector components are zero.

\begin{equation} q = w + 0 \cdot i + 0 \cdot j + 0 \cdot k \end{equation}
  • Pure quaternion

A pure quaternion, has the real part equal to zero. It can represent a vector in the space.

\begin{equation} q = 0 + x \cdot i + y \cdot j + z \cdot k \end{equation}
  • Unit quaternion

A quaternion which the norm or magnitude is equal to one.

\begin{equation} || q || = 1 \end{equation}
  • Identity quaternion

An identity quaternion when multiplied by another quaternion results in no rotation.

\begin{equation} q_i = 1 + 0 \cdot i + 0 \cdot j + 0 \cdot k \end{equation}

In similar way to identity matrices, the following is valid:

\begin{equation} q_i \cdot q_a = q_a \cdot q_i = q_a \end{equation}

Quaternion Conjugate

The conjugate of a quaternion, \(q^*\), is defined as:

\begin{equation} q^{*} = w - \vec{v} = w - (x \cdot i + y \cdot j + z \cdot k) = w + (-x) i + (-y) j + (-z) k \end{equation}

Inverse Quaternion \(q^{-1}\)

\begin{equation} q^{-1} = \frac{q^{*}}{ || q ||^2 } \end{equation}

The inverse quaternion satifies the following equality:

\begin{equation} q^{-1} \cdot q = q \cdot q^{-1} = 1 \end{equation}

Quaternion Norm

The norm (magnitude) of a quaternion is given by:

\begin{equation} || q ||^2 = q \cdot q^{*} = w^2 + x^2 + y^2 + z^2 \end{equation}

Unit quaternion

An unit quaternion \(q_u\) has norm equal to one.

\begin{equation} || q_u || = 1 \end{equation}

An unit quaternion is equal to its conjugate:

\begin{equation} q^* = q^{-1} \end{equation}

Quaternion Dot Product

A quaternion dot product is similar to vector dot product. The quaternion dot product between two quaternions q1 and q2 is defined as the sum of the product between the quaternions components.

\begin{equation} \text{dotquat}(q_1, q_2) = q_1 \cdot q_2 = w_1 \cdot w_2 + x_1 \cdot x_2 + y_1 \cdot y_2 + z_1 \cdot z_2 \end{equation}

Angle between two quaternion

The angle between two quaternions q1 and q2 is defined as:

\begin{equation} \cos \theta = \frac{ q_1 \cdot q_2 }{ || q_1 || \cdot || q_2 || } = \frac{ \text{dotquat}(q_1, q_2) }{ \text{norm}( q_1) \cdot \text{norm}(q_2) } = \frac{ w_1 \cdot w_2 + x_1 \cdot x_2 + y_1 \cdot y_2 + z_1 \cdot z_2 }{ || q_1 || \cdot || q_2 || } \end{equation}

Quaternion Sum

The sum of two quaternions q1 and q2 is:

\begin{equation} q_1 + q_2 = (w_1 + w_2) + (x_1 + x_2) i + (y_1 + y_2)j + (z_1 + z_2) k \end{equation}

Quaternion Product (a.k.a Hamilton Product)

The product p of two quaternons q1 and q2 can be computed as:

  • Where:
    • p is the quaternion product between q1 and q2
    • \(\vec{v_1} \cdot \vec{v_2}\) is the scalar product between \(\vec{v_1}\) and \(\vec{v_2}\).
    • \(\vec{v1} \times \vec{v_2}\) is the cross product between \(\vec{v_1}\) and \(\vec{v_2}\).
    • \(p_v\) is the vector component of p
    • \(p_w\) is the scalar/real component of p.
\begin{align*} p &= q_1 \cdot q_2 = ( w_1 \cdot w_2 - \vec{v_1} \cdot \vec{v_2} \quad ; \quad w_1 \cdot \vec{v_2} + w_2 \cdot \vec{v_1} + \vec{v1} \times \vec{v_2} ) \\ &= ( w_1 \cdot w_2 - \vec{v_1} \cdot \vec{v_2} ) + ( w_1 \cdot \vec{v_2} + w_2 \cdot \vec{v_1} + \vec{v1} \times \vec{v_2} ) \end{align*}

The components of vector p are:

\begin{align*} p_w &= w_1 \cdot w_2 - \vec{v_1} \cdot \vec{v_2} \\ p_v &= w_1 \cdot \vec{v_2} + w_2 \cdot \vec{v_1} + \vec{v1} \times \vec{v_2} \\ \end{align*}

The quaternion product is non commuative, as a result, the following holds:

\begin{equation} q_1 \cdot q_2 \neq q_2 \cdot q_2 \end{equation}

The quaternion product satifies the following properties:

\begin{align*} || q_1 \cdot q_2 || &= ||q_1|| \cdot || q_2 || \\ (q_1 \cdot q_2)^* &= q_2^* \cdot q_1^* \end{align*}

Quaternion product between pure quaternions

The product between two pure quaternions yields the dot product and cross simultaneously, the dot product as the real part of the operation result and cross product as the vector part of the result.

\begin{equation} p \cdot q = - (p \cdot q) + (p \times q) \end{equation}

Where:

  • Note: A pure quaternion, is the same as space vector, the real part is zero, w = 0.
  • \(p = 0 + x_p \cdot i + y_p \cdot j + z_p \cdot k\)
  • \(q = 0 + x_q \cdot i + y_q \cdot j + z_q \cdot k\)
  • \(p \cdot q\) is the dot product between the vector parts of p and q.
  • \(p \times q\) is the cross product (vector product) between p and q.

Quaternion division (MATH432 - page 11)

The division between two quaternions q1 and q2 is defined as:

\begin{equation} \frac{q_1}{q_2} = \frac{q_1 \cdot q_2^* }{ || q_2 ||^2 } = \frac{1}{ || q_2 ||^2 } ( q_1 \cdot q_2^* ) \end{equation}

Quaternion product using matrices representation

The product of two quaternions q1 and q2 can be computed in a more convenient and faster way by using matrices and column vectors multiplication, instead of dot product and cross product.

Let \([q]_v\) be the vector column representation of a quaternion:

\begin{equation} [q]_v = \begin{bmatrix} w \\ x \\ y \\ z \end{bmatrix} \end{equation}

Let \([q]_m\) be the following matrix representation of a quaternion:

\begin{equation} [q]_m = \begin{bmatrix} w & -x & -y & -z \\ x & w & -z & y \\ y & z & w & -x \\ z & -y & x & w \\ \end{bmatrix} \end{equation}

The quaternion product between q1 and q2, designated by \([q_r]_v\), can be expressed as a matrix-column vector multiplication, which is best suitable for a computer implementation.

\begin{equation} [q_r]_v = q_1 \cdot q_2 = [q_1]_m \cdot [q_2]_v = \begin{bmatrix} w_1 & -x_1 & -y_1 & -z_1 \\ x_1 & w_1 & -z_1 & y_1 \\ y_1 & z_1 & w_1 & -x_1 \\ z_1 & -y_1 & x_1 & w_1 \\ \end{bmatrix} \begin{bmatrix} w_2 \\ x_2 \\ y_2 \\ z_2 \\ \end{bmatrix} \end{equation}

By performing the previous matrix multiplication, the quaternion product becomes:

\begin{equation} [q_r]_v = q_1 \cdot q_2 = \begin{bmatrix} w_1 \cdot w_2 - x_1 \cdot x_2 - y_1 \cdot y_2 - z_1 \cdot z_2 \\ x_1 \cdot w_2 + w_1 \cdot x_2 - z_1 \cdot y_2 + y_1 \cdot z_2 \\ y_1 \cdot w_2 + z_1 \cdot x_2 + w_1 \cdot y_2 - x_1 \cdot z_2 \\ z_1 \cdot w_2 - y_1 \cdot x_2 + x_1 \cdot y_2 + w_1 \cdot z_2 \\ \end{bmatrix} \end{equation}

Rotation Quaternion

A rotation around an axis \(\hat{n}\) (unit vector) by angle \(\theta\) can be encoded by quaternion \(q_r\):

\begin{equation} q_r = \cos \frac{\theta}{2} + \sin \frac{\theta}{2} \cdot \hat{n} = \cos \frac{\theta}{2} + \sin \frac{\theta}{2} ( x_n \cdot i + y_n \cdot j + z_n \cdot k ) \end{equation}
  • Where:
    • \(\hat{n}\) is a unit vector.
    • \(\hat{n} = x_n \cdot i + y_n \cdot j + z_n \cdot k\)
    • \(|| \hat{n} || = 1\)

The norm of quaternion \(q_r = 1\), thus the following holds:

\begin{equation} || q_r || = 1 \end{equation}

The coordinate transformation from a vector in reference frame A to a reference frame B, obtained by the rotation of coordinate frame A around a unit vector \(\hat{n}\) can be computed as:

\begin{equation} q_B = q_r \cdot q_A \cdot q_r^{-1} = q_r \cdot q_A \cdot q_r^{*} \end{equation}

Where:

  • \(q_A\) is the coordinate of a vector in the reference frame A (aka coordinate system, aka coordinate frame).
  • \(q_B\) is the coordinate of a the same vector in the reference frame B.
  • \(q_A = 0 + x_A \cdot i + y_A \cdot j + z_A \cdot k\)
  • \(q_B = 0 + x_B \cdot i + y_B \cdot j + z_B \cdot k\)

The previous rotation coordinate transformation can be determined in a more efficient way by applying the following algorithm. (The-Ryg-Blog)

\begin{eqnarray*} v_t &=& 2 ( v_r \times v_a) \\ q_B &=& v_A + w_r \cdot v_t + v_r \times v_t \end{eqnarray*}

Where:

  • \([ Q ]_w\) - real part of a quaternion Q
  • \([ Q ]_v\) - vector part of a quaterion Q
  • \(w_r = \cos ( \theta / 2)\) - real part of rotation quaternion \(q_r\)
  • \(v_r = \hat{n} \cdot \sin( \theta / 2 )\) - vector part of rotation quaternion \(q_r\)
  • \(v_B\) - vector part of quaternion \(q_B\)
  • \(v_A\) - Vector part of quaternion \(q_A\)
  • \(v_r \times v_a\) - Cross product between those two vectors.

Determining rotation and axis from a normalized quaternion

Supposing that the components (w, x, y, z) of normalized quaternion q are known. The represented rotation angle, \(\theta\) and rotation axis (a.k.a orientation) vector, \(\hat{n} = [x_n \quad y_n \quad z_n]\), can be determined in the following way.

Rotation quaternion (known: x, y, z, w)

\begin{eqnarray*} q &=& w + x \cdot i + y \cdot j + z \cdot k \\ &=& \cos \frac{\theta}{2} + \sin \frac{\theta}{2} \hat{n} \\ &=& \cos \frac{\theta}{2} + \sin \frac{\theta}{2} (x_n \cdot i + y_n \cdot j + z_n \cdot k) \\ \end{eqnarray*}

Rotation angle \(\theta\)

\begin{equation} \theta = 2 \text{atan2}( || \vec{v} ||, w ) = 2 \text{atan2}( \sqrt{x^2 + y^2 + z^2} , w ) \end{equation}

Rotation axis: \(\hat{n} = x_n \cdot i + y_n \cdot j + z_n \cdot k\)

\begin{eqnarray*} x_n &=& x / \sqrt{1 - w^2} \\ y_n &=& y / \sqrt{1 - w^2} \\ z_n &=& z / \sqrt{1 - w^2} \\ \end{eqnarray*}

Where:

  • Atan2(Y, X) is the 2-argument tangent function which determines the angle quadrant.
  • \(q = w + x \cdot i + y \cdot j + z \cdot k\) - is a unitary quaternion, which components x, y and z are assumed to be known. The quaternion q is presumed to be normalized.
  • \(\theta\) is the rotation angle. Assumed to be unknown.
  • \(\hat{n}\) - is the rotation axis vector. Also assumed to be unknown, which components are \(x_n\), \(y_n\) and \(z_n\).
    • \(\hat{n} = 0 + x_n \cdot i + y_n + \cdot j + z_n \cdot k\)

Quaternion to rotation matrix conversion

The rotation matrix that transforms coordinates from reference frame A to reference frame B can be computed as: (Moti Ben-Ari - Wizmann), (Jernej Barbic)

\begin{equation} R_n = \begin{bmatrix} a^2 + b^2 - c^2 - d^2 & 2 (b \cdot c - a \cdot d) & 2 (b \cdot d + a \cdot c) \\ 2 (b \cdot c + a \cdot d ) & a^2 - b^2 + c^2 -d^2 & 2 ( c \cdot d - a \cdot b ) \\ 2 ( b \cdot d - a \cdot c ) & 2 ( c \cdot d + a \cdot b ) & a^2 - b^2 - c^2 + d^2 \\ \end{bmatrix} \end{equation}

Where:

  • a, b, c, d are the components of the unit quaternion \(q_r\)
  • \(a = \cos( \theta / 2)\)
  • \(b = x_n \cdot \sin (\theta / 2)\)
  • \(c = y_n \cdot \sin (\theta / 2)\)
  • \(d = z_n \cdot \sin (\theta / 2)\)

The matrix \(R_n\) can also be simplified to: (Moti Ben-Ari - Wizmann)

\begin{equation} R_n = 2 \begin{bmatrix} a^2 + b^2 - 1/2 & b \cdot c - a \cdot d & a \cdot c + b \cdot d \\ a \cdot d + b \cdot c & a^2 + c^2 - 1/2 & c \cdot d - a \cdot b \\ b \cdot d - a \cdot c & a \cdot b + c \cdot d & a^2 + d^2 - 1/2 \\ \end{bmatrix} \end{equation}

Coordinates from frame A can be turned into coordinates from frame B by performing the following matrix multiplication.

\begin{equation} \begin{bmatrix} x_B \\ y_B \\ z_B \end{bmatrix} = R_n \begin{bmatrix} x_A \\ y_A \\ z_A \end{bmatrix} \end{equation}

From the previous matrix \(R_n\) it is possible to determine the yaw-pitch-roll (Z-X-Y) Euler's angles. (chrobotics)

\begin{eqnarray*} \phi &=& \arctan\left( \frac{ 2 a \cdot b + 2 c \cdot d } { a^2 - b^2 - c^2 + d^2 } \right ) \\ \theta &=& -\arcsin\left( 2 b \cdot d - 2 a \cdot c \right ) \\ \psi &=& \arctan\left( \frac{ 2 a \cdot d + 2 b \cdot c}{ a^2 + b^2 - c^2 - d^2 } \right ) \\ \end{eqnarray*}

Where:

  • \(\phi\) - is the yaw angle
  • \(\theta\) - is the pitch angle
  • \(\psi\) -is the roll angle

Quaternion to homogeneous coordinate rotation matrix

The matrix \(R_n\) can be converted to a homogenous coordinate form, \(T_n\), which is more convenient, since homogenous coordinate allows expressing translations in a similar fashion to rotation transformations, just as vector-matrix multiplication.

\begin{equation} T_n = \begin{bmatrix} a^2 + b^2 - c^2 - d^2 & 2 (b \cdot c - a \cdot d) & 2 (b \cdot d + a \cdot c) & 0 \\ 2 (b \cdot c + a \cdot d ) & a^2 - b^2 + c^2 -d^2 & 2 ( c \cdot d - a \cdot b ) & 0 \\ 2 ( b \cdot d - a \cdot c ) & 2 ( c \cdot d + a \cdot b ) & a^2 - b^2 - c^2 + d^2 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

Homogeneous coordinates from frame A can be turned into coordinates of frame B by using the following expression:

\begin{equation} \begin{bmatrix} x_B \\ y_B \\ z_B \\ 1 \end{bmatrix} = T_n \begin{bmatrix} x_A \\ y_A \\ z_A \\ 1 \end{bmatrix} \end{equation}

Table with Useful Normalized Quaternions (Rotation Quaternions) - (Ogre3D Game Engine)

w x y z   Description
1 0 0 0   Identity quaternion, represents no rotation.
0 1 0 0   180° (π radians) rotation around X axis
0 0 1 0   180° rotation around Y axis
0 0 0 1   180° rotation around Z axis.
           
\(\sqrt{1/2}\) \(\sqrt{1/2}\) 0 0   90° rotation around X axis.
\(\sqrt{1/2}\) 0 \(\sqrt{1/2}\) 0   90° rotation around Y axis.
\(\sqrt{1/2}\) 0 0 \(\sqrt{1/2}\)   90° rotation around Z axis.
           
\(\sqrt{1/2}\) \(-\sqrt{1/2}\) 0 0   -90° rotation around X axis.
\(\sqrt{1/2}\) 0 \(-\sqrt{1/2}\) 0   -90° rotation around Y axis.
\(\sqrt{1/2}\) 0 0 \(-\sqrt{1/2}\)   -90° rotation around Z axis.
           

Note:

  • \(\sqrt{1/2} = \sqrt{0.5} \approx 0.70710\)
  • \(\pi = pi = \approx 3.14159265\)

Further Reading

Quaternions:

Dual Quaternion:

Quaternion Computer Implementations:

  • t::Quaternion - ROS Framework (C++)
  • Qt5 C++ Framework - QQuaternion class (C++)
  • Qt5 - Rotation Example
  • MinGfx C++ - OpenGL Wrapper (C++)
  • FQuat - Unreal Game Engine (C++)
    • Brief: "Floating point quaternion that can represent a rotation about an axis in 3-D space."
  • FTransform - Unreal Game Engine (C++)
    • Brief: "Transform composed of Scale, Rotation (as a quaternion), and Translation."
    • Note: It has three member variables: Rotation (class FQaut), Scale3D (FVector) and Translation (FVector).
  • FRotator - Unreal Gamer Engine (C++)
    • Brief: "Implements a container for rotation information. All rotation values are stored in degrees."
  • Unity3D - Quaternion (C#, CSharp - Unity Game Engine)
    • Brief: "Quaternions are used to represent rotations. They are compact, don't suffer from gimbal lock and can easily be interpolated. Unity internally uses Quaternions to represent all rotations. They are based on complex numbers and are not easy to understand intuitively. You almost never access or modify individual Quaternion components (x,y,z,w); most often you would just take existing rotations (e.g. from the Transform) and use them to construct new rotations (e.g. to smoothly interpolate between two rotations). The Quaternion functions that you use 99% of the time are: Quaternion.LookRotation, Quaternion.Angle, Quaternion.Euler, Quaternion.Slerp, Quaternion.FromToRotation, and Quaternion.identity. (The other functions are only for exotic uses.)"
  • Magnum Graphics Library - Quaternion (C++)
  • Quaternion Struct - Dotnet (C#)
  • Octave - Quaternion Library (OCtave Language, akin to Matlab)
  • Blender - MathUtils module

1.8.17 Quaternions - Combining Scale and Position

Quaternions (non-dual quaternions) can only encode and represent rotations around some orientation, which is not enough to specify position and scaling. As a result, quaternions rotation matrices need to be concatenated with translation and scaling transform matrices for computing an object's model transform matrix, which is often passed to a shader program via uniform variables.

Assuming that a rendered object has a transform member variable of type RendererTransform containing a a quaternion, for representing rotation; a scale vector and a position vector. The method get_transform() compute the rendered object transform matrix, which is the concatenation of rotation (quaternion), scaling and translation transforms. The model transform determined by get_transform() method can be passed to a shader uniform variable.

Note: Dual-quaternions, which are a combination of dual-numbers and ordinary quaternions, can express both rotation and translation.

  • Class RendererTransform:
struct RenderTransform
{
      // Rotation quaternion
      // Set to identity quaternion by default.
      // Members. The quaternion has the fields (w, x, y, z)
      quaternion rotation {1.0, 0.0, 0.0};

      // Scale vector containing scales (sx, sy, sz) of axis (X, Y, Z)
      vec3 scale(1.0, 1.0, 1.0);

      // Contains object's position
      vec3 position(tx, ty, tz);

      // ??? Get overall (end result) transform matrix that
      // will be passed to shader uniform variable.
      mat4 get_transform() const { ????? }

      void translate(float dx, float dy, float dz)
      {
          this->position.x += dx;
          this->position.y += dy;
          this->position.z += dz;
      }

      void set_position(float x, float y, float z)
      {
          this->position.x = x;
          this->position.y = y;
          this->position.z = z;
      }

      void set_scale(float sx float sy, float sz)
      {
         this->scale.x = sx;
         this->scale.y = sy;
         this->scale.z = sz;
      }
};

The transforms are often applied in the following sequence, first scaling, then rotation and finally translation.

\begin{equation} T = T_t \cdot T_r \cdot T_s = \text{Translation} \cdot \text{Rotation} \cdot \text{Scaling} \end{equation}

Where:

  • T is the total transform matrix, which is the product between intermediate transforms and later can be passed to a shader uniform variable.
  • \(T_s\) - is the scale transform, determined from the scale vector.
  • \(T_t\) - is the translation transform, computed from the position vector.
  • \(T_r\) - is the quaternion rotation transform.

Intermediate transforms:

  • Translation affine transform form position vector.
\begin{equation} T_t = \begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}
  • Scaling affine transform from scale vector.
\begin{equation} T_s = \begin{bmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}
  • Rotation affine transform obtainted from rotation quaternion.
    • Note: most linear algebrar or computer graphics math libraries provide subroutines (aka functions) or class methods for converting quaternions to 4x4 rotation matrices. Those libraries also provide subroutines for determining a quaternion's yaw, pitch and roll angles and constructing the quaternion from yaw, pitch and roll angles.
\begin{equation} T_r = \begin{bmatrix} a_{11} & a_{12} & a_{13} & 0 \\ a_{21} & a_{22} & a_{23} & 0 \\ a_{32} & a_{32} & a_{33} & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

The end transform T is determined by concatenating all intermediate transforms:

\begin{equation} T = T_t \cdot T_r \cdot T_s \end{equation} \begin{equation} T = \begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} a_{11} & a_{12} & a_{13} & 0 \\ a_{21} & a_{22} & a_{23} & 0 \\ a_{31} & a_{32} & a_{33} & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

Finally, the model transform becomes:

\begin{equation} T = \begin{bmatrix} s_x \cdot a_{11} & s_y \cdot a_{12} & s_z \cdot a_{13} & t_x \\ s_x \cdot a_{21} & s_y \cdot a_{22} & s_z \cdot a_{23} & t_y \\ s_x \cdot a_{31} & s_y \cdot a_{32} & s_z \cdot a_{33} & t_z \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}

Then, the algorithm for determing the object model matrix, designated by the transform T, could be written as the following pseudo-code:

mat4 RenderTransform::get_transform() const
{
    // Convert quaternion to a 4x4 rotation matrix (affine transform)
    mat4 model_transform = quaternion_to_rotation_transform( this->quaternion );

    // Multiply all elements of first column by X axis scale
    model_transform.comlumn[0] = scale.x * model_transform.column[0];
    // Multiply all elemnts of second column by Y axis scale
    model_transform.comlumn[1] = scale.y * model_transform.column[1];
    // Multiply all elemnts of third column by Z axis scale
    model_transform.comlumn[2] = scale.z * model_transform.column[2];

    // Set all elements of forth column as the 4x1  column vector
    // column4 = [position.x position.y position.z 1.0 ]
    model_transform.column[4] = vec4( position.x, position.y, position.z, 1.0f );

    return model_transform;
}

To rotate the rendered object, it is only necessary to multiply the quaternion variable, called 'rotation' by another quaternion. So, a C++-like pseudo-code for setting or adding a new rotation could be written as:

RenderTransform object_transform;

// Set a new rotation - overriding the old one.
void RenderTransform::set_rotation(float angle_radians, vec3 axis_direction)
{
    // Normalize axis
    vec3 axis = axis_direction.normalize();
    // Make explicit assumption the assumption that the quaternion is normalized.
    assert( norm(axis) ~= 1.0  );
    this->object_transform.quaternion = make_quaternion_from_AngleAxis(angle, axis);
}

// Add a new rotation, rotate the previous quaternion.
void RenderTransform::rotate(float angle_radians, vec3 axis_direction)
{
    vec3 axis = axis_direction.normalize();
    assert( norm(axis) ~= 1.0  );
    quaternon q = make_quaternion_from_AngleAxis(angle, axis);
    this->object_transform.quaternion = this->object_transform.quaternion * q;
}


// Set a new Yaw-Pitch-Roll Euler's angles rotation.
void RenderTransform::set_rotation_YRP(float yaw, float pitch, float roll)
{
    vec3 axis = axis_direction.normalize();
    assert( norm(axis) ~= 1.0  );
    quaternon q = make_quaternion_from_YAW_PITCH_ROLL(yaw, pitch, roll);
    assert( norm(q) ~= 1.0 );
    this->object_transform.quaternion = q;
}

See also:

1.8.18 Quaternion in Julia Language

It is worth prototyping quaternion math in a numerically-oriented programming language such as Julia Language is helpful for understanding and validating the inner works of quaternion computations. This implementation of quaternion math represents quaternion as an array of four elements and uses a matrix representation for implemeting quaternion multiplication.

# Struct for storing quaternion
# q = w + x·i + y·j + z·k
struct quatp
   arr::Array{Float64,1}
end

function quat(x, y, z, w)
  return quatp([x ; y; z; w])
end

# Turns a quaternion into 4x4 matrix representation
function quat_to_mat4x4(q::quatp)
    w, x, y, z  = q.arr
    mat = [  w -x -y -z
           ; x  w -z  y
           ; y  z  w -x
           ; z -y x w ]
    return mat
 end

function Base.getproperty(q::quatp, sym::Symbol)
    if sym == :arr
        return getfield(q, :arr)
    elseif sym == :w
        return getfield(q, :arr)[1]
    elseif sym == :x
        return getfield(q, :arr)[2]
    elseif sym == :y
        return getfield(q, :arr)[3]
    elseif sym == :z
        return getfield(q, :arr)[4]
    elseif sym == :v
        # Return only the quaternion (vector part)
        return getfield(q, :arr)[2:end]
    end
end

# Norm - quaternion magnitude, akin to vector norm
function norm(q::quatp)
   w, x, y, z = q.arr
   return sqrt(w * w + x * x + y * y + z * z)
end

# Conjugate of a quaternion
function conj(q::quatp)
  return quat(q.w, -q.x, -q.y, -q.z)
end

# Inverse of quaternion q^-1
function Base.:inv(q::quatp)
   w, x, y, z = q.arr
   norm_square =  w * w +  x * x + y * y + z * z
   return  quat(w, -x, -y, -z) / norm_square
end

# Quaternion sum
Base.:+(q1::quatp, q2::quatp) = quat(q1.w + q2.w, q1.x + q2.x, q1.y + q2.y, q1.z + q2.z)
Base.:-(q1::quatp, q2::quatp) = quat(q1.w - q2.w, q1.x - q2.x, q1.y - q2.y, q1.z - q2.z)

Base.:+(q::quatp,   w::Float64) = quat(w + q.w, q.x, q.y, q.z)
Base.:+(w::Float64, q::quatp  ) = quat(w + q.w, q.x, q.y, q.z)
Base.:-(q::quatp,   w::Float64) = quat(q.w - w, q.x, q.y, q.z)

# Mutliplication => scalar X quaternion
Base.:*(a::Float64, q::quatp) = quat( a * q.w, a * q.x, a * q.y, a * q.z)
Base.:*(q::quatp, a::Float64) = quat( a * q.w, a * q.x, a * q.y, a * q.z)

Base.:/(q::quatp, a::Float64) = quat( q.w / a, q.x / a, q.y / a, q.z / a)

# Quaternion multiplication / product
Base.:*(q1::quatp, q2::quatp) = begin
     A = quat_to_mat4x4(q1)
     return quatp( A * q2.arr )
end

# Unit quaternions i, j, k
const i = quat(0.0, 1.0, 0.0, 0.0)
const j = quat(0.0, 0.0, 1.0, 0.0)
const k = quat(0.0, 0.0, 0.0, 1.0)


# Obtain a rotation quaternion that represents a rotation
# by some angle around some direction (normalized vector 'vec')
# => The angle is given in degrees.
function rot_quat(angle::Float64, vec)
   # Normalize vector vec and store the result in v
   xv, yv, zv = vec
   x, y, z = vec / sqrt( xv * xv + yv * yv + zv * zv )
   S = cosd(angle / 2)
   C = sind(angle / 2)
   return quat(C, S * x, S * y, S * z)
end

## Vector cross product VA x VB of two column vectors (3 x 1)
function cross(va, vb)
     x, y, z = va
     T = [ 0 -z y ; z 0 -x; -y x 0]
     T * vb
 end

## Apply a rotation quaternion to a vector (column vector 3 x 1)
##  =>> Vector 3 rows and 1 column
## Algorithm from:
#     =>> https://fgiesen.wordpress.com/2019/02/09/rotating-a-single-vector-using-a-quaternion/
function apply_rot(qr::quatp, va)
   wr = qr.w # Scalar/real part of rotation quaternion
   vr = qr.v # Vector part of rotation quaternion
   vt = 2 * cross(vr, va)
   vb = va + wr * vt + cross(vr, vt)
   return vb
end

# Obtain a rotation matrix - that represents rotation
# around an arbitrary axis (vector)
# Note: The angle is should be in degrees.
#
function make_rotmat(angle::Float64, vec)
   # Normalize vector vec and store components in x, y, z
   x, y, z = vec
   x, y, z = vec / sqrt( x * x + y * y + z * z )
   # sin and cos caching
   C = cosd(angle / 2)
   S = sind(angle / 2)
   # Parameters a, b, c, d => Components of the rotation
   # quaternion
   a = C
   b = x * S
   c = y * S
   d = z * S
   # rotation matrix
   mat = [
        ( a^2 + b^2 - c^2 - d^2 )  ( 2 * (b * c - a * d )  ) ( 2 * (b * d + a * c)   )  0
     ;  ( 2 * (b * c + a * d)   )  ( a^2 - b^2 + c^2 - d^2 ) ( 2 * (c * d - a * b)   )  0
     ;  ( 2 * (b * d - a * c)   )  ( 2 * (c * d + a * b)   ) ( a^2 - b^2 - c^2 + d^2 )  0
     ;  0                          0                         0                          1
   ]
   return mat
end

Quaternion components:

# Construct a quaternion

julia> q = quat(10.5, -25.1, 4.5, 5.6)
quatp([10.5, -25.1, 4.5, 5.6])

julia> q.w # Real part
10.5

julia> q.x # Imaginary part - i axis component
-25.1

julia> q.y # Imaginary part - j axis component
4.5

julia> q.z # Imaginary part - k axis component (Z axis)
5.6

julia> q.arr # Internal array data representation
4-element Array{Float64,1}:
  10.5
 -25.1
   4.5
   5.6

julia> q.v # Vector component (i, j, k) components only
3-element Array{Float64,1}:
 -25.1
   4.5
   5.6

Quaternion conjugate and inverse:

julia> q = quat(1.0, 2.0, 3.0, 4.0)
quatp([1.0, 2.0, 3.0, 4.0])

julia> inv(q)
quatp([0.03333333333333333, -0.06666666666666667, -0.1, -0.13333333333333333])

# Expected (-1.0)
julia> q * inv(q)
quatp([1.0, 0.0, 0.0, 0.0])

# Expected (-1.0)
julia> inv(q) * q
quatp([1.0, -5.551115123125783e-17, -6.938893903907228e-18, 5.551115123125783e-17])

Normalize a quaternion:

# Expected normalized to be: (w = 0.18257, x = 0.36515, y = 0.54772, z = 0.7303)
# Test case from: Matlab Robotics Toolbox - https://www.mathworks.com/help/robotics/ref/quaternion.normalize.html

 julia> q = quat(1.0, 2.0, 3.0, 4.0)
 quatp([1.0, 2.0, 3.0, 4.0])

 julia> qn = normalize(q)
 quatp([0.18257418583505536, 0.3651483716701107, 0.5477225575051661, 0.7302967433402214])

 julia> norm(qn)
 0.9999999999999999

Test case 1 - for quaternion multiplication:

  • Test quaternion laws:
  • Expected: i * i = j * j = k * k = -1
  • Expected: i * j = k
  • Expected: j * k = i
  • Expected: k * i = j
  • Expected: j * i = -k
# ---- Show components i, j, k --------#

julia> i
quatp([0.0, 1.0, 0.0, 0.0])

julia> j
quatp([0.0, 0.0, 1.0, 0.0])

julia> k
quatp([0.0, 0.0, 0.0, 1.0])

# ----- Check multiplication of i, j, k by themselves -----------#
#

julia> i * i # Expected -1 or (-1) + 0·i + 0·j + 0·k
quatp([-1.0, 0.0, 0.0, 0.0])

julia> j * j # Expected -1
quatp([-1.0, 0.0, 0.0, 0.0])

julia> k * k # Expected -1
quatp([-1.0, 0.0, 0.0, 0.0])

#------- Check multiplication i * j, j * k, k * i and j * i
#
julia> i * j # Expected k
quatp([0.0, 0.0, 0.0, 1.0])

julia> j * i # Expected -k
quatp([0.0, 0.0, 0.0, -1.0])

julia> k * i # Expected j
quatp([0.0, 0.0, 1.0, 0.0])

julia> i * k # Expected -j
quatp([0.0, 0.0, -1.0, 0.0])

Test case 2 - for quaternion multiplication:

  • From: Matlab Aerospace Toolbox
  • q1 = [ 1.0 0.0 1.0 0.00 ]
  • q2 = [ 1.0 0.5 0.5 0.75 ]
  • Expected result =>> q1 * q2 = [ 0.5 1.25 1.5 0.25 ]
# -------- Test A ------------------------#
julia> q1 = quat(1.0, 0.0, 1.0, 0.0)
quatp([1.0, 0.0, 1.0, 0.0])

julia> q2 = quat(1.0, 0.5, 0.5, 0.75)
quatp([1.0, 0.5, 0.5, 0.75])

# ------- Test B ---------------------#

julia> q1 = 1.0 + 0.0i + 1.0j + 0.0k
quatp([1.0, 0.0, 1.0, 0.0])

julia> q2 = 1.0 + 0.5i + 0.5j + 0.75k
quatp([1.0, 0.5, 0.5, 0.75])

julia> q1 * q2
quatp([0.5, 1.25, 1.5, 0.25])

Test case 3 - for quaternion multiplication:

  • From: Maplesoft - Quaternion
  • q1 = ( -6) + (-8)i + (-5)j + 7k
  • q2 = (-14) + (-12)i + (-19)j + (-17)k
  • Expected: q1 * q2 = 12 + 402i + (-36)j + 96k
  • Expected: q2 * q1 = 12 + (-34)i + 404j + (-88)k
  • Expected: q2 + q1 = (-20) + (-20)i + (-24)j + (-10)k
julia> q1 = -6.0 + -8.0i + -5.0j + 7.0k
quatp([-6.0, -8.0, -5.0, 7.0])

julia> q2 = -14.0 + -12.0i + -19.0j + -17.0k
quatp([-14.0, -12.0, -19.0, -17.0])

julia> q1 * q2
quatp([12.0, 402.0, -36.0, 96.0])

julia> q2 * q1
quatp([12.0, -34.0, 404.0, -88.0])

julia> q1 + q2
quatp([-20.0, -20.0, -24.0, -10.0])

julia> q2 + q1
quatp([-20.0, -20.0, -24.0, -10.0])


Test algorithm - make_rotmat that computes rotation matrices around an arbitrary axis:

Define functions for computing rotation matrices:

# Rotation around X axis
function rot_x(angle::Float64)
   C = cosd(angle)
   S = sind(angle)
   return [  1   0   0  0
           ; 0   C  -S  0
           ; 0   S   C  0
           ; 0   0   0  1
          ]
end

# Rotation around X axis
function rot_y(angle::Float64)
   c = cosd(angle)
   s = sind(angle)
   return [  c   0   s  0
           ; 0   1   0  0
           ;-s   0   c  0
           ; 0   0   0  1
          ]
end

# Returns homogeneous rotation matrix around Z axis
# => The angle must be in degrees, not radians
function rot_z(angle::Float64)
   c = cosd(angle)
   s = sind(angle)
   return [  c  -s  0  0
           ; s   c  0  0
           ; 0   0  1  0
           ; 0   0  0  1
          ]
end

Test rotation matrices:

const axis_x = [1.0 0.0 0.0]';
const axis_y = [0.0 1.0 0.0]';
const axis_z = [0.0 0.0 1.0]';

# -------- Rotation around Z axis ---------- #
#

julia> rot_z(90.0)
4×4 Array{Float64,2}:
 0.0  -1.0  0.0  0.0
 1.0   0.0  0.0  0.0
 0.0   0.0  1.0  0.0
 0.0   0.0  0.0  1.0

julia> make_rotmat(90.0, axis_z)
4×4 Array{Float64,2}:
 0.0  -1.0  0.0  0.0
 1.0   0.0  0.0  0.0
 0.0   0.0  1.0  0.0
 0.0   0.0  0.0  1.0


julia> rot_z(45.0)
4×4 Array{Float64,2}:
 0.707107  -0.707107  0.0  0.0
 0.707107   0.707107  0.0  0.0
 0.0        0.0       1.0  0.0
 0.0        0.0       0.0  1.0

julia> make_rotmat(45.0, axis_z)
]4×4 Array{Float64,2}:
 0.707107  -0.707107  0.0  0.0
 0.707107   0.707107  0.0  0.0
 0.0        0.0       1.0  0.0
 0.0        0.0       0.0  1.0

julia> rot_z(125.0)
4×4 Array{Float64,2}:
 -0.573576  -0.819152  0.0  0.0
  0.819152  -0.573576  0.0  0.0
  0.0        0.0       1.0  0.0
  0.0        0.0       0.0  1.0

julia> make_rotmat(125.0, axis_z)
4×4 Array{Float64,2}:
 -0.573576  -0.819152  0.0  0.0
  0.819152  -0.573576  0.0  0.0
  0.0        0.0       1.0  0.0
  0.0        0.0       0.0  1.0


# ----- Rotation around X axis ------------#
#
julia> rot_x(0.0)
4×4 Array{Float64,2}:
 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

julia> make_rotmat(0.0, axis_x)
4×4 Array{Float64,2}:
 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


julia> rot_x(45.0)
4×4 Array{Float64,2}:
 1.0  0.0        0.0       0.0
 0.0  0.707107  -0.707107  0.0
 0.0  0.707107   0.707107  0.0
 0.0  0.0        0.0       1.0

julia> make_rotmat(45.0, axis_x)
4×4 Array{Float64,2}:
 1.0  0.0        0.0       0.0
 0.0  0.707107  -0.707107  0.0
 0.0  0.707107   0.707107  0.0
 0.0  0.0        0.0       1.0


julia> rot_x(90.0)
4×4 Array{Float64,2}:
 1.0  0.0   0.0  0.0
 0.0  0.0  -1.0  0.0
 0.0  1.0   0.0  0.0
 0.0  0.0   0.0  1.0

julia> make_rotmat(90.0, axis_x)
4×4 Array{Float64,2}:
 1.0  0.0   0.0  0.0
 0.0  0.0  -1.0  0.0
 0.0  1.0   0.0  0.0
 0.0  0.0   0.0  1.0

Test rotation quaternions:

# Rotation around X axis
function rot_xx(angle::Float64)
   C = cosd(angle)
   S = sind(angle)
   return [ 1  0  0  ; 0   C  -S   ; 0   S   C     ]
end

julia> va = [6.0 10.5 2.56]'
3×1 LinearAlgebra.Adjoint{Float64,Array{Float64,2}}:
  6.0
 10.5
  2.56

angle = 60.0 # 60 degrees

julia> vb_rotmat = rot_xx(angle) * va
3×1 Array{Float64,2}:
  6.0
  3.032974966311837
 10.373266739736605

# Rotation quaternion that represents rotation of 60 degrees around X axis.
julia> qr = rot_quat(60.0, [1.0 0.0 0.0]')
quatp([0.8660254037844386, 0.5, 0.0, 0.0])

# Apply transform to vector va
julia> vb_qr = apply_rot(qr, va)
3×1 Array{Float64,2}:
  6.0
  3.032974966311837
 10.373266739736607

1.9 Drawing Primitives

OpenGL can draw only points, lines, triangles and quads as primitives. Complex models, solids, surfaces and curves can be drawn combining those primitives.

Drawing is perfomed by using the glDrawArrays() subroutine, that draws using the vertices from the current bound VBO - Vertex Buffer Object or VAO - Vertex Array Object.

Primitives:

  • GL_POINTS => Draw a point for each vertex.
  • GL_LINES => Unconnected lines - useful for drawing grids and coordinate axis. Each two vertices are considered the beginning and the end of the current line. For instance, if there are 6 vertices, V0, V1, V2, V3, V4, V5. The primitive GL_LINES, will result in the following lines: Line(V0, V1) - line from V0 to V1; Line(V2, V3); Line(V4, V5).
  • GL_LINE_STRIP => Draw connected lines for each pair of consecutive vertices. For each, if there is a set of 4 vertices, named V0, V1, V2, V3. This primitive will draw the following lines: Line(V0, V1) - line from V0 to V1; Line(V1, V2) and Line(V2, V3). q
  • GL_LINE_LOOP => Similar, to GL_LINE_STRIP primitive, although this primitive draws a line between the first and the last vertex. This primitive is suitable for drawing poligons.
  • GL_TRIANGLES => Draws a triangle for each three vertices. This primitive is useful for drawing surfaces and solids. Any complex OpenGL surface can be approximated using triangles. This primitive draws a triangle for each three vertices. If there is a set of vertices V0, V1, V2, V3, V4 and V5, the primitive will draw the following triangles Triangle(V0, V1, V2) and Triangle(V3, V4, V5).
  • GL_TRIANGLE_FAN
  • GL_TRIANGLE_STRIP
glBindVertexArray(my_vao);

// Draw points lines - using the vertices coordinates from VAO 'my_vao'.
// (current bounded VAO)
glDrawArrays(GL_POINTS, 0, n_vertices);


// Draw unconnected lines - using the vertices coordinates from VAO 'my_vao'.
glDrawArrays(GL_LINES, 0, n_vertices);

// Draw connected lines - using the vertices coordinates from VAO 'my_vao'.
// (current bounded VAO)
glDrawArrays(GL_LINE_STRIP, 0, n_vertices);

1.10 Library GLM - OpenGL Mathematics

1.10.1 Overview

The library GLM (OpenGL math library) contains many classes and subroutines for computer graphics computations, such as: homogeneous coordinates; quaternios; 1D, 2D, 3D and homogeneous coordinates vectors; vector-matrix operations and so on. Aside those facilities, the library also provides the subroutines glm::lookAt(), for computing view matrix transform, that turns world coordinates into camera coordinates; glm::perspective() - for computing the projection matrix, that turns camera coordinates into clip-space coordinates (NDC coordinates with range -1.0 to 1.0) and also glm::ortho for computing the orthogonal perspective matrix. The GLM library is designed with syntax based on GLSL (OpenGL shading language).

Web Site:

Repository:

Other Documentations:

Type signature of most relevant GLM functions:

// Converts an input angle in degrees to radians.
//
//  angle_radians = angle_degrees * (PI / 180.0 )
//
float glm::radians(foat  degrees);

// ------ Basic Matrix Transformation =>> Useful for model matrix ---------------//
glm::mat4 glm::rotate   ( glm::mat4 const & m, float angle, glm::vec3 const & axis );
glm::mat4 glm::scale    ( glm::mat4 const & m, glm::vec3 const & factors           );
glm::mat4 glm::translate( glm::mat4 const & m, glm::vec3 const & translation       );


// ---- Camera View matrix and Camera's matrix -------------//
glm::mat4 glm::lookAt( glm::vec3 const & eye, glm::vec3 const & look, glm::vec3 const & up );
glm::mat4 glm::ortho( float left, float right, float bottom, float top, float near, float far );
glm::mat4 glm::ortho( float left, float right, float bottom, float top );
glm::mat4 glm::frustum( float left, float right, float bottom, float top, float near, float far );
glm::mat4 glm::perspective( float fovy, float aspect, float near, float far);

Subroutines for vector computations

  • glm::normalize()
    • => Normalize a vector.
  • glm::dot(vec1, vec2)
    • => Vector dot product.
  • glm::length(vec)
    • => Vector norm, aka magnitude.
  • glm::distance(vec1, vec2)
    • => Distance between two vectors thatrepresents position.
  • glm::cross(vec1, vec2)
    • => Returns the cross product between two vectors.

Subroutine for Camera Implementation

Camera's View Matrix:

  • glm::mat4 glm::lookAt (eye, look, up );
    • Brief: Return View matrix transform - which transforms world coordinates to camera's space coordinates.
    • eye => Vector containing the camera's current position in world coordinates.
    • look => Vector (Position) to where camera is looking at.
    • up => Camera orientation. Suitable fault value Y axis or (x = 0, y = 1, z = 0).

Camera's Projection Matrix:

  • glm::mat4 glm::perspective ( fovy, float aspect, near, far);
    • Returns projection matrix that transforms camera-space coordinates to clip-space coordinates (NDC - Normalized Device Coordinates within range -1.0 to 1.0). Anything out of the near, far range is not visible.
    • fovy => Field view angle in radians.
    • near => Distance to near plane.
    • far => Distance to far plane.
  • glm::mat4 glm::ortho ( left, right, bottom, top );
    • Returns a projection matrix for ortographic projection. This type projection is most suitable for 2D drawing; CAD - Computer Aided Design; Technical drawing view and on.

GLM functions - transform matrix multiplication order

Consider the subroutine, glm::translate() - which translate a coordinate system.

glm::mat4 SOURCE_MATRIX = something();
glm::mat4 OUTPUT_MATRIX =  glm::translate( SOURCE_MATRIX, glm::vec3(DX, DY, DZ));

GLM computes the OUTPUT_MATRIX by performing the matrix multiplication in the following way. The same logic is applicable to the subroutines: glm::rotate, glm::scale and so on.

OUTPUT_MATRIX = SOURCE_MATRIX * Translate_Transform(DX, DY, DZ)

                                | 1  0  0  DX |
OUTPUT_MATRIX = SOURCE_MATRIX * | 0  1  0  DY |
                                | 0  0  1  DZ |
                                | 0  0  0  1  |

Consider the following sequence of operations:

 const glm::vec4 axis_Z = glm::vec3(0, 0, 1);

// Reset model matrix to identity matrix
 glm::mat4 model(1.0);

 // Move to (X, Y) position
 // model = model * T_translate(_x, y, 0.0)
 // model = identity * T_translate(_x, y, 0.0) = T_translate(_x, y, 0.0)
 model = glm::translate( model, glm::vec3(_x, _y, 0.0)  );

 // Scale object (increase or decrease object size)
 // model = model * T_scale
 // model = T_translate * T_scale
 model =  glm::scale( model, glm::vec3(_scale, _scale, _scale) );

 // Rotate from a given angle around Z axis at current object X, Y  postion
 // model = model * T_rotation
 // model =  (T_translate * T_scale) * T_rot
 model = glm::rotate( model, glm::radians(_angle),  axis_Z);

The final value of the model matrix, is product between the following affine transforms:

model  =  Identity * T_translate * T_scale * T_rotation
model  =  T_translate * T_scale * T_rotation

1.10.2 Testing in CERN's Root REPL

Download Library Source

$ mkdir -p /tmp/temp && cd /temp

# Download source code archive
$ >> curl -o glm.zip -L https://github.com/g-truc/glm/archive/master.zip

# Extract code
$ >> unzip  glm.zip

# Enter the in the extracted directory
$ >> cd glm-master/

# List directory content
$ >> ls
cmake/  CMakeLists.txt  copying.txt  doc/  glm/  manual.md  readme.md  test/  util/

Load the library in CERN's ROOT repl

 $ >> ~/Applications/root/bin/root
ERROR in cling::CIFactory::createCI(): cannot extract standard library include paths!
Invoking:
  LC_ALL=C ccache  -O2 -DNDEBUG -xc++ -E -v /dev/null 2>&1 | sed -n -e '/^.include/,${' -e '/^ \/.*++/p' -e '}'
Results was:
With exit code 0
   ------------------------------------------------------------------
  | Welcome to ROOT 6.22/02                        https://root.cern |
  | (c) 1995-2020, The ROOT Team; conception: R. Brun, F. Rademakers |
  | Built for linuxx8664gcc on Aug 17 2020, 12:46:52                 |
  | From tags/v6-22-02@v6-22-02                                      |
  | Try '.help', '.demo', '.license', '.credits', '.quit'/'.q'       |
   ------------------------------------------------------------------

root [0]

root [0] .I .

root [0] .I .
root [1] #include <glm/glm.hpp>
root [2] #include <glm/gtc/matrix_transform.hpp>
root [3] #include <glm/gtc/type_ptr.hpp>
root [4] #include <glm/gtx/string_cast.hpp>

// Note: GLM matrices are stored in Column-major order
void show_matrix(const char* label, glm::mat4 const& m){
    std::cout << "\n [MATRIX] " << label << " = " << '\n';
    std::cout << std::fixed << std::setprecision(3);
    for(size_t i = 0; i < 4; i++)
    {
        for(size_t j = 0; j < 4; j++)
        {
            std::cout << std::setw(8) << m[j][i];
        }
        std::cout << '\n';
    }
}

Construct a matrix from column vectors:

\begin{equation} \text{colum}_0 = \begin{bmatrix} 25 \\ 40 \\ 7 \\ 0 \end{bmatrix} \quad \text{colum}_1 = \begin{bmatrix} 8 \\ 9 \\ 10 \\ 0 \end{bmatrix} \quad \text{colum}_2 = \begin{bmatrix} -5 \\ 6 \\ 9 \\ 0 \end{bmatrix} \quad \text{colum}_3 = \begin{bmatrix} 100 \\ 50 \\ 20 \\ 1 \end{bmatrix} \end{equation} \begin{equation} M = \begin{bmatrix} \text{colum}_0 &\text{colum}_1 & \text{colum}_2 & \text{colum}_3 \end{bmatrix} \end{equation} \begin{equation} M = \begin{bmatrix} 25 & 8 & -5 & 100 \\ 40 & 9 & 6 & 50 \\ 7 & 10 & 9 & 20 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{equation}
auto col0 = glm::vec4(25, 40, 7, 0)
auto col1 = glm::vec4(8, 9, 10, 0)
auto col2 = glm::vec4(-5, 6, 9, 0)
auto col3 = glm::vec4(100, 50, 20, 1)

root [9] auto M = glm::mat4(col0, col1, col2, col3)

root [24] show_matrix("M", M)

 [MATRIX] M =
  25.000   8.000  -5.000 100.000
  40.000   9.000   6.000  50.000
   7.000  10.000   9.000  20.000
   0.000   0.000   0.000   1.000

This matrix could also be constructed in the following mode:

 auto MM = glm::mat4(25, 40, 7, 0  ,8, 9, 10, 0  ,-5, 6, 9, 0 ,100, 50, 20, 1)

root [27] show_matrix("MM", MM)

 [MATRIX] MM =
  25.000   8.000  -5.000 100.000
  40.000   9.000   6.000  50.000
   7.000  10.000   9.000  20.000
   0.000   0.000   0.000   1.000

// Note: It shows the matrix columns, not the matix rows.
root [39] glm::to_string(M)
(std::string) "mat4x4((25.000000, 40.000000, 7.000000, 0.000000), (8.000000, 9.000000, 10.000000, 0.000000), (-5.000000, 6.000000, 9.000000, 0.000000), (100.000000, 50.000000, 20.000000, 1.000000))"
root [40]

Extract columns from matrix MM:

#include <glm/gtc/matrix_access.hpp>

auto col0 = glm::column(MM, 0)
auto col1 = glm::column(MM, 1)
auto col2 = glm::column(MM, 2)
auto col3 = glm::column(MM, 3)

root [58] glm::to_string(col0)
(std::string) "vec4(25.000000, 40.000000, 7.000000, 0.000000)"

root [60] glm::to_string(col1)
(std::string) "vec4(8.000000, 9.000000, 10.000000, 0.000000)"

root [62] glm::to_string(col2)
(std::string) "vec4(-5.000000, 6.000000, 9.000000, 0.000000)"

root [64] glm::to_string(col3)
(std::string) "vec4(100.000000, 50.000000, 20.000000, 1.000000)"

// Get elements from column 0 (col0)
root [65] col0[0]
(float) 25.0000f

root [66] col0[1]
(float) 40.0000f

root [67] col0[2]
(float) 7.00000f

root [68] col0[3]
(float) 0.00000f

Extract rows from matrix MM:

auto row0 = glm::row(MM, 0)
auto row1 = glm::row(MM, 1)
auto row2 = glm::row(MM, 2)
auto row3 = glm::row(MM, 3)

root [73] glm::to_string(row0)
(std::string) "vec4(25.000000, 8.000000, -5.000000, 100.000000)"

root [74] glm::to_string(row1)
(std::string) "vec4(40.000000, 9.000000, 6.000000, 50.000000)"

root [75] glm::to_string(row2)
(std::string) "vec4(7.000000, 10.000000, 9.000000, 20.000000)"

root [76] glm::to_string(row3)
(std::string) "vec4(0.000000, 0.000000, 0.000000, 1.000000)"

Modify matrix columns:

root [77] show_matrix("MM", MM)

 [MATRIX] MM =
  25.000   8.000  -5.000 100.000
  40.000   9.000   6.000  50.000
   7.000  10.000   9.000  20.000
   0.000   0.000   0.000   1.000

root [80] MM[0] = glm::vec4(-90.f, 50.0f, 32.5f, 26.0f)


root [81] show_matrix("MM", MM)

 [MATRIX] MM =
 -90.000   8.000  -5.000 100.000
  50.000   9.000   6.000  50.000
  32.500  10.000   9.000  20.000
  26.000   0.000   0.000   1.000

Obtain transpose matrix:

 root [82] auto tmat = glm::transpose(MM)
 (glm::mat<4, 4, float, glm::qualifier::packed_highp> &) @0x7f7b71f0c20c


 root [83] show_matrix("tmat", tmat)

  [MATRIX] tmat =
  -90.000  50.000  32.500  26.000
    8.000   9.000  10.000   0.000
   -5.000   6.000   9.000   0.000
  100.000  50.000  20.000   1.000

// ----- Access first column elements (column 0) ----//
//  =>> The matrix is column-major order (Fortran-like data layout),
//      not C or C++ data-layout, which is row-major order
//
// Tmat[j column-number][i row-number]

// ----- Column 0 elements ----------//

root [84] tmat[0][0]
(float) -90.0000f

root [85] tmat[1][0]
(float) 50.0000f

root [86] tmat[0][0]
(float) -90.0000f

root [87] tmat[0][1]
(float) 8.00000f

root [88] tmat[0][2]
(float) -5.00000f

root [89] tmat[0][3]
(float) 100.000f

Get pointer to first element:

root [90] float* ptr = glm::value_ptr(tmat)
(float *) 0x7f7b71f0c20c

root [91] ptr[0]
(float) -90.0000f

root [92] ptr[1]
(float) 8.00000f

root [93] ptr[15]
(float) 1.00000f

root [94] ptr[10]
(float) 9.00000f

root [95] *(ptr + 1)
(float) 8.00000f

root [96] *(ptr + 10)
(float) 9.00000f

root [97] *(ptr + 15)
(float) 1.00000f

Null 4x4 matrix:

root [53] auto zero_4x4 = glm::mat4()
(glm::mat<4, 4, float, glm::qualifier::packed_highp> &) @0x7f904e6c20e0

root [54] show_matrix("zero_4x4", zero_4x4)

 [MATRIX] zero_4x4 =
   0.000   0.000   0.000   0.000
   0.000   0.000   0.000   0.000
   0.000   0.000   0.000   0.000
   0.000   0.000   0.000   0.000

Identity matrix:

root [55] auto id_4x4 = glm::mat4(1.0)
(glm::mat<4, 4, float, glm::qualifier::packed_highp> &) @0x7f904e6c2120
root [56]
root [56] show_matrix("id_4x4", id_4x4)

 [MATRIX] id_4x4 =
   1.000   0.000   0.000   0.000
   0.000   1.000   0.000   0.000
   0.000   0.000   1.000   0.000
   0.000   0.000   0.000   1.000

Matrix translation coordinate transform:

root [58] auto t1 = glm::translate(id_4x4, glm::vec3(2.0, 10.0, 30.0))
(glm::mat<4, 4, float, glm::qualifier::packed_highp> &) @0x7f904e6c2160

root [59] show_matrix("t1", t1)

 [MATRIX] t1 =
   1.000   0.000   0.000   2.000
   0.000   1.000   0.000  10.000
   0.000   0.000   1.000  30.000
   0.000   0.000   0.000   1.000


root [62] auto t2 = glm::translate(t1, glm::vec3(-10, 100.0, 200.0))
(glm::mat<4, 4, float, glm::qualifier::packed_highp> &) @0x7f904e6c21a0

root [63] show_matrix("t2", t2)

 [MATRIX] t2 =
   1.000   0.000   0.000  -8.000
   0.000   1.000   0.000 110.000
   0.000   0.000   1.000 230.000
   0.000   0.000   0.000   1.000

Rotation around Z axis of 90 degrees:

  • glm::mat4 glm::rotate(glm::mat4 const& matrix, float angle_radians, glm::vec3 const& axis)
  • Rotate around a given axis. The angle is given in radians.
root [65] const auto axis_z = glm::vec3(0.0f, 0.0f, 1.0f);
root [66] const auto axis_y = glm::vec3(0.0f, 1.0f, 0.0f);
root [67] const auto axis_x = glm::vec3(1.0f, 0.0f, 0.0f);


/*          | cos(t)  -sin(t)   0   0  |
 *          | sin(t)   cos(t)   0   0  |
 *  Rz(t) = |  0        0       1   0  |
 *          |  0        0       0   1  |
 *
 *
 *  t_rotZ = Rz(90) x id_4x4 = Rz(90)
 */
root [71] auto t_rotZ = glm::rotate(id_4x4, glm::radians(90.0f), axis_z)
(glm::mat<4, 4, float, glm::qualifier::packed_highp> &) @0x7f904e6c2204

root [72] show_matrix("t_rotZ", t_rotZ)

 [MATRIX] t_rotZ =
  -0.000  -1.000   0.000   0.000
   1.000  -0.000   0.000   0.000
   0.000   0.000   1.000   0.000
   0.000   0.000   0.000   1.000
root [73]

 root [96] glm::to_string(t_rotZ)
 (std::string) "mat4x4((-0.000000, 1.000000, 0.000000, 0.000000), ... "
 root [97]

Scaling transformation:

root [75] auto s1 = glm::scale(id_4x4, glm::vec3(2.0, 2.0, 2.0))
(glm::mat<4, 4, float, glm::qualifier::packed_highp> &) @0x7f904e6c2244

root [76] show_matrix("s1", s1)

 [MATRIX] s1 =
   2.000   0.000   0.000   0.000
   0.000   2.000   0.000   0.000
   0.000   0.000   2.000   0.000
   0.000   0.000   0.000   1.000

root [78] s1 = glm::scale(s1, glm::vec3(5.0, 5.0, 5.0))
(glm::mat &) @0x7f904e6c2244

root [79] show_matrix("s1", s1)

 [MATRIX] s1 =
  10.000   0.000   0.000   0.000
   0.000  10.000   0.000   0.000
   0.000   0.000  10.000   0.000
   0.000   0.000   0.000   1.000
root [80]

// Apply transform to vector:

root [82] auto res = s1 * glm::vec4(2.0, 5.0, 10.0, 1.0)
(glm::vec<4, float, glm::qualifier::packed_highp> &) @0x7f904e6c2284

root [91] glm::to_string(res)
(std::string) "vec4(20.000000, 50.000000, 100.000000, 1.000000)"

root [92] res[0]
(float) 20.0000f

root [93] res[1]
(float) 50.0000f

root [94] res[2]
(float) 100.000f

root [95] res[3]
(float) 1.00000f

Scaling transformation:

root [97] auto s = glm::scale(id_4x4, glm::vec3(4.0, 5.0, 6.0))
(glm::mat<4, 4, float, glm::qualifier::packed_highp> &) @0x7f904e6c22a4

root [98] show_matrix("s", s)

 [MATRIX] s =
   4.000   0.000   0.000   0.000
   0.000   5.000   0.000   0.000
   0.000   0.000   6.000   0.000
   0.000   0.000   0.000   1.000
root [99]
root [99] s = glm::translate(s, glm::vec3(4, -5, 9))
(glm::mat &) @0x7f904e6c22a4


root [100] show_matrix("s", s)

 [MATRIX] s =
   4.000   0.000   0.000  16.000
   0.000   5.000   0.000 -25.000
   0.000   0.000   6.000  54.000
   0.000   0.000   0.000   1.000
root [101]


root [105] s = glm::scale(s, glm::vec3(5.0, 2.0, 3.0))
(glm::mat &) @0x7f904e6c22a4


root [106] show_matrix("s", s)

 [MATRIX] s =
  25.000   0.000   0.000  20.000
   0.000   4.000   0.000 -10.000
   0.000   0.000   9.000  27.000
   0.000   0.000   0.000   1.000

Inverse matrix:

root [111] show_matrix("s", s)

 [MATRIX] s =
  25.000   0.000   0.000  20.000
   0.000   4.000   0.000 -10.000
   0.000   0.000   9.000  27.000
   0.000   0.000   0.000   1.000

root [112] inv_s = glm::inverse(s)
(glm::mat<4, 4, float, glm::qualifier::packed_highp> &) @0x7f904e6c2324

root [113] show_matrix("inv_s", inv_s)

 [MATRIX] inv_s =
   0.040  -0.000   0.000  -0.800
  -0.000   0.250  -0.000   2.500
   0.000  -0.000   0.111  -3.000
  -0.000   0.000  -0.000   1.000
root [114]
root [114]

GLM Quaternions

Create an identity quaternion, which represents no rotation.

root [26] auto q = glm::quat(1.0, 0.0, 0.0, 0.0)
(glm::qua<float, glm::qualifier::packed_highp> &) @0x7fc8b47e6020

root [27] q.x
(float) 0.00000f
root [28]
root [28] q.y
(float) 0.00000f
root [29]
root [29] q.z
(float) 0.00000f
root [30]
root [30] q.w
(float) 1.00000f
root [31]
root [31]

Create a quaternion that represents a rotation of 90 degrees around Z axis:

root [39] auto qrot_z = glm::angleAxis(glm::radians(90.0f), glm::vec3{0.0f, 0.0f, 1.0f} )
(glm::qua<float, glm::qualifier::packed_highp> &) @0x7fc8b47e6050

root [40] glm::to_string(qrot_z)
(std::string) "quat(0.707107, {0.000000, 0.000000, 0.707107})"

root [42] qrot_z.w
(float) 0.707107f

root [43] qrot_z.x
(float) 0.00000f

root [44] qrot_z.y
(float) 0.00000f

root [45] qrot_z.z
(float) 0.707107f

Get quaternion angle:

// Angle in radians.
root [61] glm::angle(qrot_z)
(float) 1.57080f

// Angle in degrees
root [62] glm::angle(qrot_z) * 180.0 / M_PI
(double) 90.000003

Get quaternion rotation axis vector:

root [63] auto axis = glm::axis(qrot_z)
(glm::vec<3, float, glm::qualifier::packed_highp> &) @0x7fc8b47e6150

root [64] glm::to_string(axis)
(std::string) "vec3(0.000000, 0.000000, 1.000000)"

Determine quaternion conjugate:

// Conjugate
root [68] auto res = glm::conjugate(qrot_z)
(glm::qua<float, glm::qualifier::packed_highp> &) @0x7fc8b47e616c

root [69] glm::to_string(res)
(std::string) "quat(0.707107, {-0.000000, -0.000000, -0.707107})"

root [71] glm::to_string( qrot_z * res )
(std::string) "quat(1.000000, {0.000000, 0.000000, 0.000000})"

Determine quaternion inverse:

root [72] glm::to_string( glm::inverse(qrot_z) )
(std::string) "quat(0.707107, {-0.000000, -0.000000, -0.707107})"

Computer quaternion norm (aka magnitude):

root [74] glm::length(qrot_z)
(float) 1.00000f

Turn the previous quaternion in to a rotation matrix:

root [47] auto TrotZ1 = glm::mat4_cast(qrot_z)

root [48] show_matrix("TrotZ1", TrotZ1)

 [MATRIX] TrotZ1 =
   0.000  -1.000   0.000   0.000
   1.000   0.000   0.000   0.000
   0.000   0.000   1.000   0.000
   0.000   0.000   0.000   1.000

// ------------ Confirm result ------------//
//

root [51] auto TrotZ2 = glm::rotate(glm::mat4(1.0), glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0))

root [52] show_matrix("TrotZ2", TrotZ2)

 [MATRIX] TrotZ2 =
  -0.000  -1.000   0.000   0.000
   1.000  -0.000   0.000   0.000
   0.000   0.000   1.000   0.000
   0.000   0.000   0.000   1.000

Rotate a vector with the quaternion qrot_z:

root [57] auto v = glm::vec3{1.0f, 0.0f, 0.0f}
(glm::vec<3, float, glm::qualifier::packed_highp> &) @0x7fc8b47e6138

// Rotating a vector just requires multiplication.
root [59] auto rotated_v = qrot_z * v
(glm::vec<3, float, glm::qualifier::packed_highp> &) @0x7fc8b47e6144

root [60] glm::to_string(rotated_v)
(std::string) "vec3(0.000000, 1.000000, 0.000000)"


// Rotate the same vector with a rotation matrix.
root [79] glm::to_string( TrotZ2 * glm::vec4(1.0, 0.0, 0.0, 0.0) )
(std::string) "vec4(-0.000000, 1.000000, 0.000000, 0.000000)"

New rotation around Y axis about 60 degrees.

root [80] auto q = qrot_z
(glm::qua<float, glm::qualifier::packed_highp> &) @0x7fc8b47e617c

// ---------- Before rotation ------------
//
root [81] glm::to_string( q )
(std::string) "quat(0.707107, {0.000000, 0.000000, 0.707107})"

// -------- After rotation ----------------//
//
root [84] q = q * glm::angleAxis(glm::radians(60.0f), glm::vec3{0.0, 0.1, 0.0})
(glm::qua<float, (qualifier)0U> &) @0x7fc8b47e617c

root [85] glm::to_string( q )
(std::string) "quat(0.612372, {-0.035355, 0.035355, 0.612372})"

// --------- Get Rotation Matrix -----------//

root [86] auto m = glm::mat4_cast(q)

root [88] show_matrix("m", m)

 [MATRIX] m =
   0.248  -0.752   0.000   0.000
   0.747   0.248   0.087   0.000
  -0.087   0.000   0.995   0.000
   0.000   0.000   0.000   1.000

// -------- Get total rotation angle and axis ---------//
//

// Get new rotation axis
root [89] auto axis = glm::axis(q)
(glm::vec<3, float, glm::qualifier::packed_highp> &) @0x7fc8b47e61cc

root [90] glm::to_string( axis )
(std::string) "vec3(-0.044721, 0.044721, 0.774597)"

// Get rotation angle around new axis in radians
root [91] glm::angle(q)
(float) 1.82348f

// Get rotation angle around new axis in degrees
root [92] glm::angle(q) * 180.0 / M_PI
(double) 104.47752


//--------- Get Yaw, Pitch and Roll angles --------------------//
//
root [94] glm::yaw(q) * 180.0 / M_PI
(double) 4.9681836

root [95] glm::pitch(q) * 180.0 / M_PI
(double) 0.0000000

root [96] glm::roll(q) * 180.0 / M_PI
(double) 90.000003

1.11 [DRAFT] Debugging Computer Graphics Code

Outline of techniques for debugging computer graphics code:

  • Use right-hand rule for finding rotation direction around some rotation axis.
  • Use right-hand rule for finding the result vector of a vector-product operation.
  • Print/display transform matrices.
  • Use assertions for validating code assumptions during runtime and abort the process execution if the assertion predicate does not hold.
  • Use print statements for tracing the code execution flow.
  • Draw X, Y, Z axis lines for visual debugging.
  • Check shader compilation error message and error code.
  • Check OpenGL error codes for every OpenGL subroutine calls.
  • Simulate and prototype linear algebra parts in a numerically-oriented or scientific programming language such Matlab, Octave or Julia.
  • Use a debugger, such as Windows Debugger, GDB, LLDB - LVM Debugger, in the case of runtime crash due to failed assertions, std::abort() calls, uncaught exceptions, Unix signals or segmentation faults.
  • Use 3D standard test models for testing the 3D scene rendering.

Assertions

The subroutine glGetUniformLocation() returns -1 as error code when it fails to find a shader's uniform variable location named 'u_projection'.

// Get shader uniform variable location for projection matrix
// See shader code: "uniform mat4 projection;"
const GLint u_proj  = glGetUniformLocation(prog, "u_projection");
assert( u_proj >= 0 && "Failed to find u_projection uniform variable" );

Assertions should be used for non-recoverable errors, checking pre-conditions, post-conditons and invariant. Since assertions are eliminated on non-debug builds, this assertion should be replaced by a proper error checking. However, in this case, assertion are useful for a prototyping code and during development time as a scaffolding for a future error handling.

Checking OpenGL error code

 #define GL_CHECK(funcall)\
     do { \
         (funcall); \
         GLint error = glGetError();  \
         if(error == GL_NO_ERROR){ break; } \
         std::fprintf(stderr, " [OPENGL ERROR] Error code = %d ; line = %d ; call = '%s'  \n" \
                       , error, __LINE__, #funcall ); \
         abort(); \
     } while(0)

// -------- Usage ---------------//

   GL_CHECK( glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(model) ) );
   GL_CHECK( glBindVertexArray(vao) );
   GL_CHECK( glDrawArrays(draw_type, 0, n_vertices) );

1.12 Minimal C++ GLFW project

The following code draws a square and a triangle using the OpenGL retained mode API. Before the rendering takes place, data must be upload to the VBO (Vertex Buffer Object), allocated on the GPU-side, via glBufferData() call that sends the data to the previous bound VBO through glBindBuffer() call. Then, on the rendering loop and on every frame, the buffer data layout must described with glVertexAttribPointer() before drawing via call to glDrawArrays() subroutine, which draw vertices from the current bound buffer.

Subroutines that modifies global state may cause unintended behavior, so it is a good practice to unset the affected global state when the current global state is no longer needed. For instance, if the current VBO no longer needs to be bound, this global state can be disabled by calling glBindBuffer(GL_ARRAY_BUFFER, 0).

OpenGL subroutines used:

void glGenBuffers(GLsizei n, GLuint* buffers);
  • glBindBuffer() => Bind some buffer object, a.k.a enable, only one buffer can be bound at a time. (The VBO is a global state).
void glBindBuffer(GLenum target,nt buffer);
  • glBufferData() =>> Send data to current bound buffer object that was bound via glBindBuffer() call.
void glBufferData( GLenum      target,
                   GLsizeiptr  size,
                   const void* data,
                   GLenum      usage
                   );
void glEnableVertexAttribArray(GLuint index);
void glVertexAttribPointer( GLuint      index,
                            GLint       size,
                            GLenum      type,
                            GLboolean   normalized,
                            GLsizei     stride,
                            const void* pointer);
  • glDrawArrays() => Render OpenGL primitives from current bound buffer object's data. Those primitives are: lines, triangles or quads and so on.
void glDrawArrays(GLenum mode, GLint first, GLsizei count);

Screenshot

opengl-draw2d-raw.png

Source Code

  • This CMakeLists.txt building script saves the user from manually downloading, building, installing and configuring OpenGL Glew and GLFW companion libraries. This file automatically downloads the GLFW library, that provides window system abstraction and OpenGL context; and a pre-compiled GLEW library (Windows only) that provides platform-agnostic OpenGL function pointer loading.
  • The CMakeLists.txt (version 2) contains a macro for defining OpenGL applications without the boilerplate needed for linking against OpenGL companion libraries and statically linking against MINGW runtimme libraries.

File: CMakeLists.txt (Version 1)

cmake_minimum_required(VERSION 3.5)
project(OpenGL_Draw2D_VBO)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)

# Set GLFW Options before FectchContent_MakeAvailable
set( GLFW_BUILD_EXAMPLES OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_TESTS    OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_DOCS     OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_INSTALL  OFF CACHE BOOL  "GLFW lib only" )

# Donwload GLFW library
FetchContent_Declare(
  glfwlib
  URL   https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
)
FetchContent_MakeAvailable(glfwlib)

# Download pre-compiled GLEW when building under Windows NT OS (x64)
IF(WIN32)
   FetchContent_Declare(
      glew-release 
      URL  https://ufpr.dl.sourceforge.net/project/glew/glew/2.1.0/glew-2.1.0-win32.zip
      # No longer valid URL 
      ## URL     https://megalink.dl.sourceforge.net/project/glew/glew/2.1.0/glew-2.1.0-win32.zip
   )
   FetchContent_MakeAvailable(glew-release)
   include_directories( ${glew-release_SOURCE_DIR}/include  ${glm_SOURCE_DIR} )        
   link_directories(  ${glew-release_SOURCE_DIR}/lib/Release/x64 )

   set( GLEW_LIB_PATH1 ${glew-release_SOURCE_DIR}/lib/Release/x64/glew32.lib )
   set( GLEW_LIB_PATH2 ${glew-release_SOURCE_DIR}/lib/Release/x64/glew32s.lib )
ENDIF()

   # ======= T A R G E T S ============================#
   #                                                   #


       add_executable( draw2d-raw  draw2d-raw.cpp )
target_link_libraries( draw2d-raw  glfw
                                   OpenGL::GL
                                   ${GLEW_LIB_PATH1} ${GLEW_LIB_PATH2}  )

# For MINGW (GCC Ported to Windows)                              
IF(MINGW)
   # Statically link against MINGW dependencies
   # for making easier to deploy on other machines. 
   target_link_options( draw2d-raw PRIVATE                                 
                            -static-libgcc
                            -static-libstdc++

                            -Wl,-Bstatic,--whole-archive -lwinpthread
                              -Wl,--no-whole-archive                                 
                            )
ENDIF()       


# Copy GLEW DLL shared library to same directory as the executable.                              
IF(WIN32)                                
   add_custom_command(TARGET draw2d-raw POST_BUILD 
                  COMMAND ${CMAKE_COMMAND} -E copy_if_different
                  "${glew-release_SOURCE_DIR}/bin/Release/x64/glew32.dll"              
                  $<TARGET_FILE_DIR:draw2d-raw>)
ENDIF()  

File: CMakeLists.txt (Version 2) - This version provides a macro for automating the definition of OpenGL projects with ADD_OPENGL command.

cmake_minimum_required(VERSION 3.5)
project(OpenGL_Draw2D_VBO)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)

# Set GLFW Options before FectchContent_MakeAvailable
set( GLFW_BUILD_EXAMPLES OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_TESTS    OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_DOCS     OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_INSTALL  OFF CACHE BOOL  "GLFW lib only" )

# Donwload GLFW library
FetchContent_Declare(
  glfwlib
  URL   https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
)
FetchContent_MakeAvailable(glfwlib)

# Download pre-compiled GLEW when building under Windows NT OS (x64)
IF(WIN32)
   FetchContent_Declare(
      glew-release 
      URL  https://ufpr.dl.sourceforge.net/project/glew/glew/2.1.0/glew-2.1.0-win32.zip   
      # https://github.com/nigels-com/glew/archive/glew-2.2.0.zip
   )
   FetchContent_MakeAvailable(glew-release)
   include_directories( ${glew-release_SOURCE_DIR}/include  ${glm_SOURCE_DIR} )        
   link_directories(  ${glew-release_SOURCE_DIR}/lib/Release/x64 )

   set( GLEW_LIB_PATH1 ${glew-release_SOURCE_DIR}/lib/Release/x64/glew32.lib )
   set( GLEW_LIB_PATH2 ${glew-release_SOURCE_DIR}/lib/Release/x64/glew32s.lib )
ENDIF()


MACRO(ADD_OPENGL_APP target sources)
   add_executable( ${target} ${sources} )
   message([TRACE] " Add OpenGL executable = ${target} ")

   target_link_libraries( ${target} glfw
                                    OpenGL::GL
                                    ${GLEW_LIB_PATH1}
                                    ${GLEW_LIB_PATH2} )

    IF(MINGW)
        # Statically link against MINGW dependencies
        # for making easier to deploy on other machines. 
        target_link_options( ${target} PRIVATE                                 
                                 -static-libgcc
                                 -static-libstdc++
                                 -Wl,-Bstatic,--whole-archive -lwinpthread
                                   -Wl,--no-whole-archive                                    
                                 )
     ENDIF()       


     # Copy GLEW DLL shared library to same directory as the executable.                 
     IF(WIN32)                           
        add_custom_command(TARGET ${target} POST_BUILD 
                       COMMAND ${CMAKE_COMMAND} -E copy_if_different
                       "${glew-release_SOURCE_DIR}/bin/Release/x64/glew32.dll"              
                       $<TARGET_FILE_DIR:${target}>)
     ENDIF()                                       
ENDMACRO()     

     # ======= T A R G E T S ============================#
     #                                                   #

ADD_OPENGL_APP( draw2-raw draw2d-raw.cpp )

File: CMakeLists.txt (Version 3) - last resort if the SourceForge link no longer works. In this case, it is assumed that Glew library was download manually from SourceForge via the URL (glew-2.1.0 - download). Then the ZIP glew-2.1.0-win32.zip is saved in the project root directory and extracted to a directory named ./glew-2.1.0/

# Used when Glew library is download manually 
cmake_minimum_required(VERSION 3.5)
project(OpenGL_Draw2D_VBO)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)

# Set GLFW Options before FectchContent_MakeAvailable
set( GLFW_BUILD_EXAMPLES OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_TESTS    OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_DOCS     OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_INSTALL  OFF CACHE BOOL  "GLFW lib only" )

# Donwload GLFW library
FetchContent_Declare(
  glfwlib
  URL   https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
)
FetchContent_MakeAvailable(glfwlib)

# Download pre-compiled GLEW when building under Windows NT OS (x64)
IF(WIN32)
   include_directories( ${CMAKE_CURRENT_LIST_DIR}/glew-2.1.0/include  ${glm_SOURCE_DIR} )        
   link_directories(  ${CMAKE_CURRENT_LIST_DIR}/glew-2.1.0/lib/Release/x64 )

   set( GLEW_LIB_PATH1  ${CMAKE_CURRENT_LIST_DIR}/glew-2.1.0/lib/Release/x64/glew32.lib )
   set( GLEW_LIB_PATH2  ${CMAKE_CURRENT_LIST_DIR}/glew-2.1.0/lib/Release/x64/glew32s.lib )
ENDIF()

   # ======= T A R G E T S ============================#
   #                                                   #

       add_executable( draw2d-raw  draw2d-raw.cpp )
target_link_libraries( draw2d-raw  glfw
                                   OpenGL::GL
                                   ${GLEW_LIB_PATH1} ${GLEW_LIB_PATH2}  )

# For MINGW (GCC Ported to Windows)                              
IF(MINGW)
   # Statically link against MINGW dependencies
   # for making easier to deploy on other machines. 
   target_link_options( draw2d-raw PRIVATE                                 
                            -static-libgcc
                            -static-libstdc++

                            -Wl,-Bstatic,--whole-archive -lwinpthread
                              -Wl,--no-whole-archive                                 
                            )
ENDIF()       


# Copy GLEW DLL shared library to same directory as the executable.                              
IF(WIN32)                                
   add_custom_command(TARGET draw2d-raw POST_BUILD 
                  COMMAND ${CMAKE_COMMAND} -E copy_if_different
                  "${CMAKE_CURRENT_LIST_DIR}/glew-2.1.0/bin/Release/x64/glew32.dll"              
                  $<TARGET_FILE_DIR:draw2d-raw>)
ENDIF()   

Downloading glew manually with curl for usage with the previous CMakeLists.txt file:

 $ curl -L -o glew32.zip  https://ufpr.dl.sourceforge.net/project/glew/glew/2.1.0/glew-2.1.0-win32.zip

 # File hash (file signature) - unique identifier for the file  
 $ md5sum glew32.zip 
 32a72e6b43367db8dbea6010cd095355  glew32.zip

 # File hash (file signature) - unique identifier for the file  
 $ sha256sum glew32.zip 
 80cfc88fd295426b49001a9dc521da793f8547ac10aebfc8bdc91ddc06c5566c  glew32.zip

 # File hash (file signature) - unique identifier for the file  
 $ sha1sum glew32.zip 
 f505a67ad884b75648639a9b01bf0245bfcfa731  glew32.zip

$ unzip -l glew32.zip 
 Archive:  glew32.zip
   Length      Date    Time    Name
 ---------  ---------- -----   ----
         0  2017-07-31 08:46   glew-2.1.0/
         0  2017-07-31 08:41   glew-2.1.0/bin/
         0  2017-07-31 08:42   glew-2.1.0/bin/Release/
         0  2017-07-31 08:42   glew-2.1.0/bin/Release/Win32/
    389632  2017-07-31 08:42   glew-2.1.0/bin/Release/Win32/glew32.dll
    482304  2017-07-31 08:42   glew-2.1.0/bin/Release/Win32/glewinfo.exe
    294912  2017-07-31 08:42   glew-2.1.0/bin/Release/Win32/visualinfo.exe
         0  2017-07-31 08:42   glew-2.1.0/bin/Release/x64/
    422912  2017-07-31 08:42   glew-2.1.0/bin/Release/x64/glew32.dll
    539648  2017-07-31 08:42   glew-2.1.0/bin/Release/x64/glewinfo.exe
    338432  2017-07-31 08:42   glew-2.1.0/bin/Release/x64/visualinfo.exe
    ... ... ... ... ... ... ... ... ... ... ... ... ... ... .. 
    ... ... ... ... ... ... ... ... ... ... ... ... ... ... .. 

         0  2017-07-31 08:42   glew-2.1.0/lib/Release/
         0  2017-07-31 08:46   glew-2.1.0/lib/Release/Win32/
    712098  2017-07-31 08:42   glew-2.1.0/lib/Release/Win32/glew32.lib
   2443636  2017-07-31 08:42   glew-2.1.0/lib/Release/Win32/glew32s.lib
         0  2017-07-31 08:46   glew-2.1.0/lib/Release/x64/
    701288  2017-07-31 08:42   glew-2.1.0/lib/Release/x64/glew32.lib
   2584968  2017-07-31 08:42   glew-2.1.0/lib/Release/x64/glew32s.lib
      3870  2017-07-31 08:46   glew-2.1.0/LICENSE.txt
 ---------                     -------
  10662952                     47 files

 $ unzip -x glew32.zip 
 Archive:  glew32.zip
    creating: glew-2.1.0/
    creating: glew-2.1.0/bin/
    creating: glew-2.1.0/bin/Release/
    creating: glew-2.1.0/bin/Release/Win32/
   inflating: glew-2.1.0/bin/Release/Win32/glew32.dll  
   inflating: glew-2.1.0/bin/Release/Win32/glewinfo.exe 
  ... ... ... ... ... ... ... ... ... ... ... ... ... ...  
  ... ... ... ... ... ... ... ... ... ... ... ... ... ...  
    inflating: glew-2.1.0/lib/Release/Win32/glew32.lib  
   inflating: glew-2.1.0/lib/Release/Win32/glew32s.lib  
    creating: glew-2.1.0/lib/Release/x64/
   inflating: glew-2.1.0/lib/Release/x64/glew32.lib  
   inflating: glew-2.1.0/lib/Release/x64/glew32s.lib  
   inflating: glew-2.1.0/LICENSE.txt  

File: xmake.lua

  • Compilation script for Xmake building system, that uses Lua as embedded scripting language and as a DSL - Domain-Specific Language. The disadvantage of this building system are: poor IDE support and the lack of ability for downloading sources directly like CMake's FetchContent_Declare statement. Another issue is that new packages can only be added by sending pull requests to Xmake packages repository.
  • XMake packages used in this sample project:
add_rules("mode.debug", "mode.release")

add_requires("glm", "glew", "glfw")

target("draw2d")
  set_kind("binary")
  add_files("./draw2d-raw.cpp")
  add_packages("glew", "glfw", "glm")

File: draw2d-raw.cpp

#include <iostream>
#include <vector>
#include <array>
#include <cmath>
#include <cassert>

// -------- OpenGL headers ---------//
//
#define GL_GLEXT_PROTOTYPES 1 // Necessary for OpenGL >= 3.0 functions
#define GL3_PROTOTYPES      1 // Necessary for OpenGL >= 3.0 functions


#if defined(_WIN32)
   #include <windows.h>
   #include <GL/glew.h>
#endif

#include <GL/gl.h>
#include <GLFW/glfw3.h>

#include <GL/glu.h>
// #include <GL/glut.h>


struct Vertex2D
{
    GLfloat x;
    GLfloat y;
};

int main(int argc, char** argv)
{
    GLFWwindow* window;

    // Initialize GLFW 
    if (!glfwInit()){ return -1; }


    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(640, 480, "2D Drawing raw VBO buffers", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);
    // glClearColor(0.0f, 0.5f, 0.6f, 1.0f);
    glClearColor(0.0f, 0.0f, 0.0, 1.0f);

    // --- GLEW should always be initialized after an OpenGL context.
    #if _WIN32
    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK) {
        std::cerr << "GLEW::Error : failed to initialize GLEW"<< std::endl;
    }
    std::fprintf(stderr, " [TRACE] Glew Initialized Ok. \n");
    #endif


    Vertex2D triangle_points[3] = {
        Vertex2D{-0.25,   -0.25}
      , Vertex2D{ 0.00,   0.25}
      , Vertex2D{ 0.25,  -0.25}
    };

    Vertex2D square_points[4] = {
        { -0.3,  -0.3 }
      , { -0.3,   0.3 }
      , {  0.3,   0.3 }
      , {  0.3,  -0.3 }
    };

    // ================== Triangle Buffer ====================//
    //

    GLuint vbo_triangle_ = 0;

    // Create an OpenGL VBO buffer
    // =>> void glGenBuffers (GLsizei n, GLuint *buffers);
    glGenBuffers(  1                // Number of buffers that will be instantiated
                 , &vbo_triangle_   // Address of first element or address of array
                                    // that results will be written to.
                 );
    assert( vbo_triangle_ != 0 );

    // Set this buffer as the current buffer - Only one buffer can be bound at a time.
    // =>> void glBindBuffer (GLenum target, GLuint buffer)
    glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_);

    // Upload data to current VBO buffer in GPU
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_points), triangle_points, GL_STATIC_DRAW);

    // Unbind current buffer in order to avoid unintended behaviors
    // as the subroutine glBindBuffer() modifies has a global state.
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // ================ Square / Vertex Buffer Object 2 ==================//
    //

    GLuint vbo_square_ = 0;
    // Instiate buffer - gets a handle or token for
    // a buffer allocated on GPU-side.
    glGenBuffers(1, &vbo_square_);
    // Check for error
    assert( vbo_square_ != 0);
    // Bind Current buffer (Affects global state)
    glBindBuffer(GL_ARRAY_BUFFER, vbo_square_);
    // Upload data to the GPU
    glBufferData(GL_ARRAY_BUFFER, sizeof(square_points), square_points, GL_STATIC_DRAW);
    // Unbind buffer
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    GLint shader_attr = 0;

    //  ======= R E N D E R  - L O O P ============//
    //
    while ( !glfwWindowShouldClose(window) )
    {
        glClear(GL_COLOR_BUFFER_BIT);

        // ====== BEGIN RENDERING ============//

            // ------------ Draw triangle --------------//
            //
            glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_);
            glEnableVertexAttribArray(shader_attr);

            // Describe buffer data layout (binary layout)
            // =>> glVertexAttribPointer ( GLuint index, GLint size, GLenum type
            //                           , GLboolean normalized, GLsizei stride, const void *pointer);
            //
            glVertexAttribPointer(shader_attr // GLint  index         => Shader attribute location, 0 for now
                                , 2           // GLint  size          => 2 components (X, Y) of type GLfloat
                                , GL_FLOAT    // GLemum type          => Type of each component
                                , GL_FALSE    // GLboolean normalized
                                , 0           // GLsizei stride
                                , nullptr     // const void* pointer
                                );
            // Draw arrays using the content of buffer
            // Plot 1 triangle (each triangle has 3 vertices)
            glDrawArrays(GL_TRIANGLES, 0, 3);

            // Disable global state
            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glDisableVertexAttribArray(0);

            //------------ Draw Square -------------------------//
            //

            #if 1
            glBindBuffer(GL_ARRAY_BUFFER, vbo_square_);
            glEnableVertexAttribArray(shader_attr);
            glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
            // Plot 4 vertices
            glDrawArrays(GL_LINE_LOOP, 0, 4);

            // Disable global state
            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glDisableVertexAttribArray(shader_attr);
            #endif

        // ====== END RENDERING ==============//

        /* Swap front and back buffers */
        glfwSwapBuffers(window);
        /* Poll for and process events */
        glfwPollEvents();

        if( glfwGetKey(window, 'Q' ) == GLFW_PRESS )
        {
             std::cout << " [TRACE] User typed Q =>> Shutdown program. Ok. " << '\n';
             break;
        }
    }

    glfwTerminate();
    return 0;
}

Building on Linux

$ cmake -H. -B_build_linux -DCMAKE_BUILD_TYPE=Debug
$ cmake --build _build_linux --target 

# Run 
$ _build_linux/draw2d-raw 

Building with dockercross tool (cross compilation for Windows)

$ docker run -it --rm -v $PWD:/cwd -w /cwd --entrypoint=bash dockcross/windows-static-x64

$ cmake -H. -B_build_cross -DCMAKE_BUILD_TYPE=Debug
$ cmake --build _build_cross --target

Building without entering docker shell:

$ docker run -it --rm -v $PWD:/cwd -w /cwd --entrypoint=bash dockcross/windows-static-x64 cmake -H. -B_build_cross -DCMAKE_BUILD_TYPE=Debug
$ docker run -it --rm -v $PWD:/cwd -w /cwd --entrypoint=bash dockcross/windows-static-x64 cmake --build _build_cross --target

List build directory:

$ ls _build_cross
CMakeCache.txt CMakeFiles  Makefile  _deps  cmake_install.cmake  draw2d-raw.exe  glew32.dll

Run executable with wine on Linux:

$ wine _build_cross/draw2d-raw.exe 

Building on Windows with MINGW (GCC ported to Windows)

$ cmake -H. -B_build_mingw -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug
$ cmake --build _build_mingw --target

Run executable:

# On Window
$ _build_mingw\draw2d-raw.exe 

# on Linux via wine 
$ wine _build_mingw\draw2d-raw.exe  

Building with Xmake building system

Display Xmake version:

$ xmake --version | grep -i "cross-platform" 2> /dev/null
xmake v2.6.9+202208081520, A cross-platform build utility based on Lua

Enter XMake - Lua interactive REPL (Read-Eval-Print-Loop):

$ xmake lua 
xmake> 

xmake> -- Get host operating system
xmake> os.host()
< "linux"

xmake> -- Get host architecture
xmake> os.arch()
< "x86_64"

xmake> -- Open Root Linux's directory
xmake> os.execv("xdg-open /")
< 1: 0
< 2: nil

xmake> -- Check whether file exists
xmake> os.exists("./draw2d-raw.cpp")
< true
xmake> os.exists("./draw2d-raw.cpp.back")
< false

xmake> -- Get current directory
xmake> os.curdir()
< "/home/user/opengl1"

xmake> -- Get project directory
xmake> os.projectdir()
< "/home/user/opengl1"

xmake> -- Get scripting directory 
xmake> os.scriptdir()
< "/home/user/.local/share/xmake/plugins/lua"

xmake> -- Download file from http server
xmake> http = import("net.http")
xmake> http.download("https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip", "glfw-source.zip")
xmake> os.exists("glfw-source.zip")
< true

xmake> -- Extract archive 
xmake> r = import("utils.archive")
xmake> r.extract("./glfw-source.zip", "/tmp/glfw-src")


-- Check current platform (All return false in the REPL)
xmake> is_plat("windows")
< false
xmake> is_plat("linux")
< false
xmake> is_plat("unix")
< false

xmake> -- Check whether the current mode is Debug or Release (All fail in the REPL)
xmake> is_mode("debug")
< false
xmake> is_mode("release")
< false

xmake> -- Join paths 
xmake> p = path.join( os.projectdir(), "build", "path1", "path2", "app.exe")
xmake> p
< "/home/user/opengl1/build/path1/path2/app.exe"


-- Environment variable 
xmake> os.getenv("HOME")
< "/home/user"

xmake> os.getenv("SHELL")
< "/bin/bash"

xmake> os.getenv("TERM")
< "xterm-256color"

List repositories:

  • $ xrepo list-repo
$ xrepo list-repo
global repositories:
    build-artifacts https://gitlab.com/xmake-mirror/build-artifacts.git main 
    xmake-repo https://gitlab.com/tboox/xmake-repo.git master 
    builtin-repo /home/user/.local/share/xmake/repository 

3 repositories were found!

Search Xmake packages:

  • $ xrepo search <PACKAGE>
$ xrepo search boost
The package names:
    boost: 
      -> boost-1.79.0: Collection of portable C++ source libraries. (in xmake-repo)

$ xrepo search glut
The package names:
    glut: 
      -> freeglut-3.2.2: A free-software/open-source alternative to the OpenGL Utility Toolkit (GLUT) library. (in xmake-repo)
      -> glut: OpenGL utility toolkit (in xmake-repo)

Install package:

  • $ xrepo install <PACKAGE>
 $ xrepo install boost

note: install or modify (m) these packages (pass -y to skip confirm)?
in xmake-repo:
  -> bzip2 1.0.8 [from:boost]
  -> boost 1.79.0 
please input: y (y/n/m)
y
  => download https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz .. ok

List package information:

  • $ xrepo info <PACKAGE>
 $ >> xrepo info glm
The package info of project:
    require(glm): 
      -> description: OpenGL Mathematics (GLM)
      -> version: 0.9.9+8
      -> urls:
         -> https://github.com/g-truc/glm/archive/0.9.9.8.tar.gz
            -> 7d508ab72cb5d43227a3711420f06ff99b0a0cb63ee2f93631b162bfe1fe9592
         -> https://github.com/g-truc/glm.git
            -> 0.9.9+8
      -> repo: xmake-repo https://gitlab.com/tboox/xmake-repo.git master
      -> cachedir: /home/user/.xmake/cache/packages/2208/g/glm/0.9.9+8
      -> installdir: /home/user/.xmake/packages/g/glm/0.9.9+8/65b1ad153bda4a43b0454eba7969327f
      -> searchdirs: 
      -> searchnames: 0.9.9.8.tar.gz, glm.git, glm-0.9.9+8.tar.gz, glm-0.9.9+8
      -> fetchinfo: 0.9.9+8
          -> sysincludedirs: /home/user/.xmake/packages/g/glm/0.9.9+8/65b1ad153bda4a43b0454eba7969327f/include
          -> version: 0.9.9+8
      -> platforms: all
      -> requires:
         -> plat: linux
         -> arch: x86_64
         -> configs:
            -> debug: false
            -> pic: true
            -> shared: false
      -> configs:
      -> configs (builtin):
         -> debug: Enable debug symbols. (default: false)
         -> shared: Build shared library. (default: false)
         -> pic: Enable the position independent code. (default: true)
         -> lto: Enable the link-time build optimization. (type: boolean)
         -> vs_runtime: Set vs compiler runtime.
            -> values: {"MT","MTd","MD","MDd"}
         -> toolchains: Set package toolchains only for cross-compilation.
         -> cflags: Set the C compiler flags.
         -> cxflags: Set the C/C++ compiler flags.
         -> cxxflags: Set the C++ compiler flags.
         -> asflags: Set the assembler flags.
      -> references:
         -> 220814: /dev/shm/.xmake1000/220814/xrepo/working
         -> 220813: /home/user/opengl1

List project information:

  • $ xmake show
$ xmake show

The information of xmake:
    version: 2.6.9+202208081520
    host: linux/x86_64
    programdir: /home/user/.local/share/xmake
    programfile: /home/user/.local/bin/xmake
    globaldir: /home/user/.xmake
    tmpdir: /dev/shm/.xmake1000/220808
    workingdir: /home/user/opengl1
    packagedir: /home/user/.xmake/packages
    packagedir(cache): /home/user/.xmake/cache/packages/2208

The information of project: 
    plat: linux
    arch: x86_64
    mode: release
    buildir: build
    configdir: /home/user/opengl1/.xmake/linux/x86_64
    projectdir: /home/user/opengl1
    projectfile: /home/user/opengl1/xmake.lua

Configure building for Linux:

  • $ xmake f -p linux
 $ >> xmake f -p linux
checking for architecture ... x86_64

Building:

  • $ xmake build
 $ >> xmake build -v
[ 25%]: ccache compiling.release draw2d-raw.cpp
/usr/bin/gcc -c -m64 -fvisibility=hidden -fvisibility-inlines-hidden -O3 -DGLEW_NO_GLU -D_GLFW_X11 -DGLFW_INCLUDE_NONE \
               -isystem /home/user/.xmake/packages/g/glew/2.2.0/2ac0c6940a2140f7b6506fcd8240f406/include \
               -isystem /home/user/.xmake/packages/g/glfw/3.3.8/aa037e96cc854680a09e144c4b5c8fe0/include \
               -isystem /usr/include/X11/dri -isystem /home/user/.xmake/packages/g/glm/0.9.9+8/65b1ad153bda4a43b0454eba7969327f/include \
               -DNDEBUG -o build/.objs/draw2d/linux/x86_64/release/draw2d-raw.cpp.o draw2d-raw.cpp

[ 50%]: linking.release draw2d
/usr/bin/g++ -o build/linux/x86_64/release/draw2d build/.objs/draw2d/linux/x86_64/release/draw2d-raw.cpp.o -m64 \
               -L/home/user/.xmake/packages/g/glew/2.2.0/2ac0c6940a2140f7b6506fcd8240f406/lib \
               -L/home/user/.xmake/packages/g/glfw/3.3.8/aa037e96cc854680a09e144c4b5c8fe0/lib \
                -L/usr/lib/x86_64-linux-gnu -s \
                -lglew -lglfw3 -lOpenGL -lXrandr -lXinerama -lXcursor -lXrender -lX11 -lXi -lXfixes \
                -lXext -lxcb -lssl -lcrypto -lffi -lz -lXau -lXdmcp -lGL -ldl -lpthread
[100%]: build ok!

Inspecting compilation output:

$ tree build
build
└── linux
    └── x86_64
        └── release
            └── draw2d

3 directories, 1 file

Running the default xmake target:

  • $ xmake run
$ xmake run

Running a specific xmake target:

  • $ xmake run draw2d
$ xmake run draw2d

Generate file containing compilation commands:

$ xmake project -k compile_commands
create ok!

$ cat compile_commands.json 
[
{
  "directory": "/home/user/opengl1",
  "arguments": ["/usr/bin/gcc", "-c", "-m64", "-fvisibility=hidden", "-fvisibility-inlines-hidden", "-O3", "-DGLEW_NO_GLU", "-D_GLFW_X11", "-DGLFW_INCLUDE_NONE", "-I", "/home/user/.xmake/packages/g/glew/2.2.0/2ac0c6940a2140f7b6506fcd8240f406/include", "-I", "/home/user/.xmake/packages/g/glfw/3.3.8/aa037e96cc854680a09e144c4b5c8fe0/include", "-I", "/usr/include/X11/dri", "-I", "/home/user/.xmake/packages/g/glm/0.9.9+8/65b1ad153bda4a43b0454eba7969327f/include", "-DNDEBUG", "-o", "build/.objs/draw2d/linux/x86_64/release/draw2d-raw.cpp.o", "draw2d-raw.cpp"],
  "file": "draw2d-raw.cpp"
}]

Generate a CMake project file (note - this file must not be under version control):

  • $ xmake project -k cmake
$ xmake project -k cmake
create ok!

CMake file generated by Xmake (CMakeLists.txt):

# this is the build file for project 
# it is autogenerated by the xmake build system.
# do not edit by hand.

# project
cmake_minimum_required(VERSION 3.15.0)
cmake_policy(SET CMP0091 NEW)
project(draw2d LANGUAGES CXX C)

# target
add_executable(draw2d "")
set_target_properties(draw2d PROPERTIES OUTPUT_NAME "draw2d")
set_target_properties(draw2d PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/build/linux/x86_64/release")
target_include_directories(draw2d PRIVATE
    /home/user/.xmake/packages/g/glew/2.2.0/2ac0c6940a2140f7b6506fcd8240f406/include
    /usr/include/X11/dri
    /home/user/.xmake/packages/g/glfw/3.3.6/703496809fe34548a9dbcf268bfa4db5/include
    /home/user/.xmake/packages/g/glm/0.9.9+8/65b1ad153bda4a43b0454eba7969327f/include
)
target_compile_definitions(draw2d PRIVATE
    GLEW_NO_GLU
    _GLFW_X11
    GLFW_INCLUDE_NONE
)
target_compile_options(draw2d PRIVATE
    $<$<COMPILE_LANGUAGE:C>:-m64>
    $<$<COMPILE_LANGUAGE:CXX>:-m64>
    $<$<COMPILE_LANGUAGE:C>:-DNDEBUG>
    $<$<COMPILE_LANGUAGE:CXX>:-DNDEBUG>
)
if(MSVC)
    target_compile_options(draw2d PRIVATE $<$<CONFIG:Release>:-Ox -fp:fast>)
else()
    target_compile_options(draw2d PRIVATE -O3)
endif()
if(MSVC)
else()
    target_compile_options(draw2d PRIVATE -fvisibility=hidden)
endif()
if(MSVC)
    set_property(TARGET draw2d PROPERTY
        MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
target_link_libraries(draw2d PRIVATE
    glew
    X11
    xcb
    ssl
    crypto
    ffi
    z
    Xau
    Xdmcp
    glfw3
    OpenGL
    Xrandr
    Xinerama
    Xcursor
    Xrender
    Xi
    Xfixes
    Xext
    GL
    dl
    pthread
)
target_link_directories(draw2d PRIVATE
    /home/user/.xmake/packages/g/glew/2.2.0/2ac0c6940a2140f7b6506fcd8240f406/lib
    /usr/lib/x86_64-linux-gnu
    /home/user/.xmake/packages/g/glfw/3.3.6/703496809fe34548a9dbcf268bfa4db5/lib
)
target_link_options(draw2d PRIVATE
    -m64
)
target_sources(draw2d PRIVATE
    draw2d-raw.cpp
)

Now it is possible to edit the project with any IDE or smart editor that supports CMake, including Visual Studio Code - Vscode or CLion:

# Edit project with vscode using the CMakeLists.txt as project file
$ vscode .  

Building with Xmake building system on Windows

Configure XMake for using MinGW (GCC Ported to Windows)

$ xmake f -p mingw 

Show project information:

$ xmake show

The information of xmake:
    version: 2.7.1+master.1acf248c0
    host: windows/x64
    programdir: C:\Program Files\xmake
    programfile: C:\Program Files\xmake\xmake.exe
    globaldir: C:\Users\ghostuser\AppData\Local\.xmake
    tmpdir: C:\Users\ghostuser\AppData\Local\Temp\.xmake\220902
    workingdir: D:\opengl
    packagedir: C:\Users\ghostuser\AppData\Local\.xmake\packages
    packagedir(cache): C:\Users\ghostuser\AppData\Local\.xmake\cache\packages\2209

The information of project:
    plat: mingw
    arch: x86_64
    mode: release
    buildir: build
    configdir: D:\opengl\.xmake\windows\x64
    projectdir: D:\opengl
    projectfile: D:\opengl\xmake.lua

Build the application showing all compilation and linking commands:

$ xmake build -v

[ 25%]: cache compiling.release draw2d-raw.cpp
"C:\\Program Files\\mingw-w64\\x86_64-8.1.0-posix-seh-rt_v6-rev0\\mingw64\\bin\\x86_64-w64-mingw32-g++" -c \
    -m64 -fvisibility=hidden -fvisibility-inlines-hidden -O3 -DGLEW_NO_GLU -DGLEW_STATIC -DGLFW_INCLUDE_NONE \
    -isystem C:\Users\ghostuser\AppData\Local\.xmake\packages\g\glew\2.2.0\f1937cf4c620461cac69417973f0b9e2\include \
    -isystem C:\Users\ghostuser\AppData\Local\.xmake\packages\g\glfw\3.3.8\580b60fd05d941e2abb98f9baa25683e\include \
    -isystem C:\Users\ghostuser\AppData\Local\.xmake\packages\g\glm\0.9.9+8\53c948164ca94a25beb29600f690b730\include -DNDEBUG \
    -o build\.objs\draw2d\mingw\x86_64\release\draw2d-raw.cpp.obj draw2d-raw.cpp

[ 50%]: linking.release draw2d.exe
"C:\\Program Files\\mingw-w64\\x86_64-8.1.0-posix-seh-rt_v6-rev0\\mingw64\\bin\\x86_64-w64-mingw32-g++" \
    -o build\mingw\x86_64\release\draw2d.exe build\.objs\draw2d\mingw\x86_64\release\draw2d-raw.cpp.obj -m64 \
    -LC:\Users\ghostuser\AppData\Local\.xmake\packages\g\glew\2.2.0\f1937cf4c620461cac69417973f0b9e2\lib \
    -LC:\Users\ghostuser\AppData\Local\.xmake\packages\g\glfw\3.3.8\580b60fd05d941e2abb98f9baa25683e\lib \
    -s -lglew32s -lglfw3 -lopengl32 -lgdi32
[100%]: build ok!

Run the executable:

$ xmake run 

Generate CMakeLists.txt:

$ xmake project -k cmake 

Build the project using the CMakeListst.txt file:

$ cmake -H. -B_build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug

$ cmake --build _build --target all
[ 50%] Building CXX object CMakeFiles/draw2d.dir/draw2d-raw.cpp.obj
[100%] Linking CXX executable ..\build\mingw\x86_64\release\draw2d.exe
[100%] Built target draw2d

File: CMakeLists.txt - generated by XMake

# this is the build file for project 
# it is autogenerated by the xmake build system.
# do not edit by hand.

# project
cmake_minimum_required(VERSION 3.15.0)
cmake_policy(SET CMP0091 NEW)
project(draw2d LANGUAGES CXX C)

# target
add_executable(draw2d "")
set_target_properties(draw2d PROPERTIES OUTPUT_NAME "draw2d")
set_target_properties(draw2d PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/build/mingw/x86_64/release")
target_include_directories(draw2d PRIVATE
    C:/Users/ghostuser/AppData/Local/.xmake/packages/g/glew/2.2.0/f1937cf4c620461cac69417973f0b9e2/include
    C:/Users/ghostuser/AppData/Local/.xmake/packages/g/glfw/3.3.8/580b60fd05d941e2abb98f9baa25683e/include
    C:/Users/ghostuser/AppData/Local/.xmake/packages/g/glm/0.9.9+8/53c948164ca94a25beb29600f690b730/include
)
target_compile_definitions(draw2d PRIVATE
    GLEW_NO_GLU
    GLEW_STATIC
    GLFW_INCLUDE_NONE
)
target_compile_options(draw2d PRIVATE
    $<$<COMPILE_LANGUAGE:C>:-m64>
    $<$<COMPILE_LANGUAGE:CXX>:-m64>
    $<$<COMPILE_LANGUAGE:C>:-DNDEBUG>
    $<$<COMPILE_LANGUAGE:CXX>:-DNDEBUG>
)
if(MSVC)
    target_compile_options(draw2d PRIVATE $<$<CONFIG:Release>:-Ox -fp:fast>)
else()
    target_compile_options(draw2d PRIVATE -O3)
endif()
if(MSVC)
else()
    target_compile_options(draw2d PRIVATE -fvisibility=hidden)
endif()
if(MSVC)
    set_property(TARGET draw2d PROPERTY
        MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
target_link_libraries(draw2d PRIVATE
    glew32s
    glfw3
    opengl32
    opengl32
    gdi32
)
target_link_directories(draw2d PRIVATE
    C:/Users/ghostuser/AppData/Local/.xmake/packages/g/glew/2.2.0/f1937cf4c620461cac69417973f0b9e2/lib
    C:/Users/ghostuser/AppData/Local/.xmake/packages/g/glfw/3.3.8/580b60fd05d941e2abb98f9baa25683e/lib
)
target_link_options(draw2d PRIVATE
    -m64
)
target_sources(draw2d PRIVATE
    draw2d-raw.cpp
)

1.13 Minimal Dlang (D) GLFW and XMake project

This section provides a minimal OpenGL-GLFW project in Dlang (D) programming language with XMake building system, that uses Lua as an embedded scripting language and as a DSL (Domain Specific Language) for describing a projects in multiple programming languages including C, C++, DLang, Fortran, Rust, Zim, Nim and so on.

The Lua scripting file, xmake.lua describes the project compilation process that uses multiple programming languages, such as D (DLang) and C programming language, used by Glew and GLFw libraries.

Project Files

File: xmake.lua

add_rules("mode.debug", "mode.release")

add_requires("glew", "glfw")

target("draw-dlang")
  set_kind("binary")
  add_files("draw_dlang.d")
  add_packages("glew", "glfw")
  -- Customize action $ xmake run draw-dlang 
  on_run(function (target)
    print(" [XMAKE HOOK] Running a DLang - D plus OpenGL application.")
     -- print(" [TARGET] target = ", target)
    print(" [XMAKE HOOK] Running file = ", target:targetfile())
    print(" [XMAKE HOOK] Target directory = ", target:targetdir())
     -- Run executable (target file) generated by compilation.
    os.run(target:targetfile())
 end)

-- Pseudo target for running a script 
target("script")
  on_run(function()
    print(" [XMAKE-SCRIPT] Running a script in this target.") 
  end)

File: draw_dlang.d

import core.stdc.stdio;
import std.range: empty;
import str = std.string;
import std.stdio: writeln;

const     GLFW_RELEASE    = 0;
const int GLFW_PRESS      = 1;
const int GL_ARRAY_BUFFER = 0x8892;
const int GL_LINE_LOOP    = 0x0002;
const int GL_TRIANGLES    = 0x0004;
const int GL_FLOAT        = 0x1406;
const int GL_STATIC_DRAW  = 0x88E4;
const int GL_FALSE        = 0;
const int GL_TRUE         = 1;

struct Vertex2D
{
    GLfloat x;
    GLfloat y;
}

int main()
{
    writeln("GLFW with DLang - D Programming language");

    if (!glfwInit()){ return -1; }

    /* Create a windowed mode window and its OpenGL context */
    GLFWwindow* window = glfwCreateWindow(640, 480, "2D Drawing with Dlang, OpenGL and GLFW", null, null);

    if (!window)
    {
        glfwTerminate();
        return -1;
    }

     /* Make the window's context current */
    glfwMakeContextCurrent(window);
    // glClearColor(0.0f, 0.5f, 0.6f, 1.0f);
    glClearColor(0.0f, 0.0f, 0.0, 1.0f);

    Vertex2D[3] triangle_points = [
        Vertex2D(-0.25,   -0.25)
      , Vertex2D( 0.00,   0.25)
      , Vertex2D( 0.25,  -0.25)
    ];


    Vertex2D[4] square_points = [
        { -0.3,  -0.3 }
      , { -0.3,   0.3 }
      , {  0.3,   0.3 }
      , {  0.3,  -0.3 }
    ];


    // Create an OpenGL VBO buffer and send vertices to GPU
    // =>> void glGenBuffers (GLsizei n, GLuint *buffers);
    GLuint vbo_triangle_ = 0;
    glGenBuffers(1, &vbo_triangle_ );
    glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_);
    glBufferData(GL_ARRAY_BUFFER, triangle_points.sizeof, &triangle_points, GL_STATIC_DRAW);

    GLuint vbo_square_ = 0;
    glGenBuffers(1, &vbo_square_);
    assert( vbo_square_ != 0);
    glBindBuffer(GL_ARRAY_BUFFER, vbo_square_);
    // glBufferData(GL_ARRAY_BUFFER, sizeof(square_points), square_points, GL_STATIC_DRAW);
    glBufferData(GL_ARRAY_BUFFER, square_points.sizeof, &square_points, GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    GLint shader_attr = 0;

    while ( !glfwWindowShouldClose(window) )
    {

        glfwSwapBuffers(window);

        glEnableVertexAttribArray(shader_attr);

        // ---------- Draw Triangle -----------------//
        //===========================================//
        glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_);
        glVertexAttribPointer(
            // GLint  index => Shader attribute location, 0 for now
            shader_attr 
            // GLint  size  => 2 components (X, Y) of type GLfloat
          , 2           
            // GLemum type  => Type of each component
          , GL_FLOAT    
            // GLboolean normalized
          , GL_FALSE    
            // GLsizei stride
          , 0           
            // const void* pointer
          , null       
          );
        // Plot 1 triangle (each triangle has 3 vertices)
        glDrawArrays(GL_TRIANGLES, 0, 3);


        // ----------- Draw Square ------------------// 
        //===========================================//
        glBindBuffer(GL_ARRAY_BUFFER, vbo_square_);
        glEnableVertexAttribArray(shader_attr);
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, null);
        // Plot 4 vertices
        glDrawArrays(GL_LINE_LOOP, 0, 4);


        glfwPollEvents();

        if( glfwGetKey(window, 'Q' ) == GLFW_PRESS )
        {
            printf("Shutdown program Ok. \n");
             break;
        }

    }


    printf(" [TRACE] Application started Ok. \n");
    glfwTerminate();
    return 0;
}

   // ------------------ D E F I N I T I O N S   -------------------------//


// Opaque types 
alias GLFWwindow = void;
alias GLFWmonitor = void;


alias GLenum     = uint;
alias GLboolean  = char; 
alias GLbitfield = uint;  
alias GLvoid     = void; 
alias GLbyte     = char; 
alias GLshort    = short;
alias GLint      = int;
alias GLubyte    = char;
alias GLushort   = ushort;
alias GLuint     = uint;
alias GLsizei    = int;
alias GLfloat    = float;
alias GLclampf   = float;
alias GLdouble   = double;
alias GLclampd   = double;

alias khronos_ssize_t  = long;
alias khronos_intptr_t = long;
alias GLsizeiptr       = khronos_ssize_t;
alias GLintptr         = khronos_intptr_t;

extern(C) int   glfwInit(); 
extern(C) void  glfwTerminate();
extern(C) void  glfwMakeContextCurrent(GLFWwindow* window);
extern(C) int   glfwWindowShouldClose(GLFWwindow* window);
extern(C) int   glfwGetKey(GLFWwindow* window, int key);
extern(C) void  glfwPollEvents();
extern(C) void  glfwSwapBuffers(GLFWwindow* window);
extern(C) void  glClear(GLbitfield);


extern(C) GLFWwindow* glfwCreateWindow(int width, int height, const char* title
                            , GLFWmonitor* monitor, GLFWwindow* share);


// ----- OpenGL APIs ---------------------- //
//
extern(C) void glBindBuffer (GLenum target, GLuint buffer);
extern(C) void glClearColor( GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha );
extern(C) void glGenBuffers (GLsizei n, GLuint *buffers);
extern(C) void glEnableVertexAttribArray (GLuint index);
extern(C) void glDrawArrays( GLenum mode, GLint first, GLsizei count );

extern(C) void glBufferData (GLenum target, GLsizeiptr size
                            , const void *data, GLenum usage);

extern(C) void glVertexAttribPointer (GLuint index, GLint size, GLenum type
                                    , GLboolean normalized, GLsizei stride
                                    , const void *pointer);

Building with XMake

Build only the target draw-dlang:

$ xmake build -v draw-dlang  

checking for dmd ... /home/user/dlang/dmd-2.100.0/linux/bin64/dmd
checking for the dlang compiler (dc) ... dmd
checking for /home/user/dlang/dmd-2.100.0/linux/bin64/dmd ... ok
checking for flags (-O -release -inline -boundscheck=off) ... ok
[ 25%]: compiling.release draw_dlang.d
/home/user/dlang/dmd-2.100.0/linux/bin64/dmd -c -m64 -O -release -inline -boundscheck=off \
          -I/home/user/.xmake/packages/g/glew/2.2.0/2ac0c6940a2140f7b6506fcd8240f406/include \
          -I/home/user/.xmake/packages/g/glfw/3.3.8/aa037e96cc854680a09e144c4b5c8fe0/include \
          -I/usr/include/X11/dri -ofbuild/.objs/draw-dlang/linux/x86_64/release/draw_dlang.d.o draw_dlang.d
checking for dmd ... /home/user/dlang/dmd-2.100.0/linux/bin64/dmd
checking for the dlang linker (dcld) ... dmd

[ 50%]: linking.release draw-dlang
/home/user/dlang/dmd-2.100.0/linux/bin64/dmd -m64 \
          -L-L/home/user/.xmake/packages/g/glew/2.2.0/2ac0c6940a2140f7b6506fcd8240f406/lib \
          -L-L/home/user/.xmake/packages/g/glfw/3.3.8/aa037e96cc854680a09e144c4b5c8fe0/lib \
          -L-L/usr/lib/x86_64-linux-gnu -L-s -L-lglew -L-lglfw3 -L-lOpenGL -L-lXrandr -L-lXinerama -L-lXcursor \
          -L-lXrender -L-lX11 -L-lXi -L-lXfixes -L-lXext -L-lxcb -L-lssl -L-lcrypto -L-lffi \
          -L-lz -L-lXau -L-lXdmcp -L-lGL -L-ldl -L-lpthread \
          -ofbuild/linux/x86_64/release/draw-dlang \
          build/.objs/draw-dlang/linux/x86_64/release/draw_dlang.d.o
[100%]: build ok!

Run the scripting target:

$ xmake run script
 [XMAKE-SCRIPT] Running a script in this target.

Run the target draw-dlang:

$ xmake run draw-dlang
 [XMAKE HOOK] Running a DLang - D plus OpenGL application.
 [XMAKE HOOK] Running file =  build/linux/x86_64/release/draw-dlang
 [XMAKE HOOK] Target directory =  build/linux/x86_64/release

Screenshot:

opengl-draw2d-dlang1.png

1.14 Minimal SDL project

The next code uses the SLD (Simple Direct Media Layer) library instead of GLFW for window system abstraction. The advantage of SDL over GLFW is that, SDL supports more platforms and is able to deal with audio and image loading, which are essential features for rich multimedia applications. This CMake script automatically downloads SDL and defines a macro for making it easier to create OpenGL apps with SDL statically linked.

SDL Web Sites:

Project Files

File: CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(OPENGL_SDL_project)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)
# Download GLM (OpenGL math library for matrix and vectors transformation)
FetchContent_Declare(
  glm  
  URL  https://github.com/g-truc/glm/archive/0.9.8.zip
)
FetchContent_MakeAvailable(glm)
include_directories(${glm_SOURCE_DIR} )

# Donwload library SLD - Simple Direct Media Layer 
FetchContent_Declare(
  sdl  
  URL    https://github.com/libsdl-org/SDL/archive/refs/tags/release-2.0.14.zip 
)
FetchContent_MakeAvailable(sdl)
# find_package( SDL REQUIRED )

file(MAKE_DIRECTORY  ${sdl_SOURCE_DIR}/headers/SDL2)
file(COPY  ${sdl_SOURCE_DIR}/include/ 
           DESTINATION ${sdl_SOURCE_DIR}/headers/SDL2 
           FILES_MATCHING PATTERN "*.h"
           )

               add_library( sdl-include  INTERFACE )
target_include_directories( sdl-include  INTERFACE ${sdl_SOURCE_DIR}/headers )
# Option for making this target IDE-friendly
IF(false)
target_sources( sdl-include INTERFACE  
                              ${sdl_SOURCE_DIR}/headers/SDL2/SDL.h   
                              ${sdl_SOURCE_DIR}/headers/SDL2/SDL_opengl.h  
                           )

ENDIF()

# Download pre-compiled GLEW when building under Windows NT OS (x64)
IF(WIN32)
   FetchContent_Declare(
      glew-release 
      URL   https://ufpr.dl.sourceforge.net/project/glew/glew/2.1.0/glew-2.1.0-win32.zip 
      # https://github.com/nigels-com/glew/archive/glew-2.2.0.zip
   )
   FetchContent_MakeAvailable(glew-release)
   include_directories( ${glew-release_SOURCE_DIR}/include  ${glm_SOURCE_DIR} )        
   link_directories(  ${glew-release_SOURCE_DIR}/lib/Release/x64 )

   set( GLEW_LIB_PATH1 ${glew-release_SOURCE_DIR}/lib/Release/x64/glew32.lib )
   set( GLEW_LIB_PATH2 ${glew-release_SOURCE_DIR}/lib/Release/x64/glew32s.lib )
ENDIF()

MACRO(ADD_OPENGL_APP target sources)
   add_executable( ${target} ${sources} )
   message([TRACE] " Add OpenGL executable = ${target} ")

   target_link_libraries( ${target} OpenGL::GL SDL2-static sdl-include 
                                    ${GLEW_LIB_PATH1}
                                    ${GLEW_LIB_PATH2} )

    IF(MINGW)
        # Statically link against MINGW dependencies
        # for making easier to deploy on other machines. 
        target_link_options( ${target} PRIVATE                                 
                                 -static-libgcc
                                 -static-libstdc++
                                 -Wl,-Bstatic,--whole-archive -lwinpthread
                                   -Wl,--no-whole-archive                                    
                                 )
     ENDIF()       


     # Copy GLEW DLL shared library to same directory as the executable.                 
     IF(WIN32)                           
        add_custom_command(TARGET ${target} POST_BUILD 
                       COMMAND ${CMAKE_COMMAND} -E copy_if_different
                       "${glew-release_SOURCE_DIR}/bin/Release/x64/glew32.dll"              
                       $<TARGET_FILE_DIR:${target}>)
     ENDIF()                                       
ENDMACRO()     


      #========== Targets Configurations ================#

ADD_OPENGL_APP( opengl-sdl opengl-sdl.cpp )

File: xmake.lua (See: libsdl package)

add_rules("mode.debug", "mode.release")

add_requires("glm", "glew", "libsdl 2.0.14")

target("draw2d")
  set_kind("binary")
  add_files("./opengl-sdl.cpp")
  add_packages("glm", "glew", "libsdl")

File: opengl-sdl.cpp

#include <iostream>
#include <string> 
#include <cassert>

#if defined(_WIN32)
   #include <windows.h>
   #include <GL/glew.h>
#endif

#define GL_GLEXT_PROTOTYPES 1
#define GL3_PROTOTYPES      1
#include <GL/gl.h> 
#include <GL/glu.h> 

#define SDL_MAIN_HANDLED
#include <SDL2/SDL.h>
#include <SDL2/SDL_opengl.h>

// --------- OpenGL Math Library ------------//
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/string_cast.hpp>

// Wrapper for 2D vertex coordinates
struct Vertex2D{  GLfloat x, y;  };
// Wrapper for RGB colors
struct ColorRGB { GLfloat r, g, b; };

constexpr ColorRGB COLOR_R = { 1.0, 0.0, 0.0  }; // Red
constexpr ColorRGB COLOR_G = { 0.0, 1.0, 0.0  }; // Green
constexpr ColorRGB COLOR_B = { 0.0, 0.0, 1.0  }; // Blue

void compile_shader(GLuint m_program, const char* code, GLenum type);

void send_buffer( GLuint* pVao, GLuint* pVbo, GLsizei sizeBuffer
                , void* pBufffer, GLint   shader_attr, GLint size
                , GLenum type);

extern const char* shader_vertex; 
extern const char* shader_fragment;

const glm::mat4 matrix_identity = glm::mat4(1.0);


int main() 
{
    std::puts(" [TRACE] OpenGL SDL started Ok. ");

    int window_x = 100, window_y = 100, window_w = 500, window_h = 400;
    SDL_Window* window = SDL_CreateWindow(  
                                "OpenGL with SLD library"
                               , window_x, window_y  // Window position 
                               , window_w, window_h  // Window width and height 
                               , SDL_WINDOW_OPENGL   // Bitwise-OR flags  
                               );

    SDL_GLContext ctx = SDL_GL_CreateContext(window);

    // --- Initialize GLEW library after OpenGL context --------- //
    #if _WIN32
    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK) {
        std::cerr << "GLEW::Error : failed to initialize GLEW" << std::endl;
        std::abort();
    }
    std::fprintf(stderr, " [TRACE] Glew Initialized Ok. \n");
    #endif

    // ====== C O M P I L E  - S H A D E R ================== // 

    GLuint prog = glCreateProgram();
    compile_shader(prog, shader_vertex,   GL_VERTEX_SHADER   ) ;
    compile_shader(prog, shader_fragment, GL_FRAGMENT_SHADER );
    glUseProgram(prog);

    // Get shader uniform variable location for projection matrix
    // See shader code: "uniform mat4 projection;"
    const GLint u_proj  = glGetUniformLocation(prog, "u_projection");
    assert( u_proj >= 0 && "Failed to find uniform variable" );

    // Get shader uniform variable  location for model matrix.
    const GLint u_model  = glGetUniformLocation(prog, "u_model");
    assert( u_model >= 0 && "Failed to find uniform variable" );

    // Get shader attribute location - the function glGetAttribLocation
    // returns (-1) on error.
    const GLint attr_position = glGetAttribLocation(prog, "position");
    assert( attr_position >= 0 && "Failed to get attribute location" );
    std::fprintf(stderr, " [TRACE] Attribute location attr_position = %d \n"
                , attr_position);

    // Get shader index of color attribute variable.
    const GLint attr_color = glGetAttribLocation(prog, "color");
    assert( attr_color >= 0);

    glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(matrix_identity) );
    glUniformMatrix4fv(u_proj,  1, GL_FALSE, glm::value_ptr(matrix_identity) );

    // ======== U P L O A D - T O - G P U ==============================// 

    constexpr size_t NUM_VERTICES = 3;

    float a = 0.25;

    Vertex2D vertices[NUM_VERTICES] = {
      {  0.0  ,   a * sqrt(3.0f) } // Vertex 0  
    , { -2 * a,  -2 * a          } // Vertex 1  
    , {  2 * a,  -2 * a          } // Vertex 2  
    };

    ColorRGB colors[NUM_VERTICES] = {
          {0.4, 0.2, 0.67} 
        , {0.8, 0.6, 0.30}
        , {0.2, 0.6, 0.10}  
    };

    // Always initialize with zero (0), otherwise, the values
    // of those variables will be undefined. (Undefine Behavior)
    GLuint vao_triangle = 0;
    GLuint vbo_vertices = 0;
    GLuint vbo_colors   = 0;

    // Send vertices to GPU VBO (vbo_vertices)
    send_buffer(  &vao_triangle    // Ponter to VAO [OUTPUT]
                , &vbo_vertices    // Pointer VBO   [OUTPUT]
                , sizeof(vertices) // Size of buffer sent to GPU (VBO memory)
                , vertices         // Pointer to buffer
                , attr_position    // Shader attribute location
                , 2                // Each vertex has 2 coordinates (X, Y)
                , GL_FLOAT         // Each vertex coordinate has type GLfloat (float)
                );

    // Send colors to GPU VBO (vbo_colors)
    send_buffer(  &vao_triangle   // Pointer Vertex Array object
                , &vbo_colors     // Pointer Vertex buffer object handle (aka token)
                , sizeof(colors)  // Buffer size
                , colors          // Pointer to buffer (addrress of first buffer element)
                , attr_color      // Shader attribute location
                , 3               // Each color has 3 coordinates (R, G, B)
                , GL_FLOAT        // Each color coordiante has type GLfloat
                );

    glm::mat4 model; 

    // ======== R E N D E R I N G - L O O P ============================//

    bool is_running = true; 

    while( is_running )
    {
        SDL_Event event;
        // std::fprintf(stderr, " [TRACE] Waiting SDL event \n"); 

        while( SDL_PollEvent(&event) )
        {
            // std::fprintf(stderr, " [TRACE] Polling event \n");

            if( event.type == SDL_QUIT ){ is_running == false; }

            if( event.type == SDL_WINDOWEVENT 
                && event.window.event == SDL_WINDOWEVENT_CLOSE )
            {  is_running = false; 
               SDL_Quit(); 
            }
        }

        glViewport(0, 0, window_w, window_h);
        // OpenGL Background color (black for avoiding eye strain)
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // === BEGIN REDERING ========// 

            // ------ Draw triangle 1 ------ //  
            glBindVertexArray(vao_triangle);
            model = matrix_identity;
            model = glm::translate(model, glm::vec3(0.35, 0.22, 0.0));
            glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(model) );
            glDrawArrays(GL_TRIANGLES, 0, 3);

            // ------ Draw triangle 2 ------ //
            glBindVertexArray(vao_triangle);
            model = matrix_identity;
            model = glm::translate(model, glm::vec3(-0.5, -0.4, 0.0));
            model = glm::scale(model, glm::vec3(0.5, 0.5, 0.5 ) );
            model = glm::rotate(model, glm::radians(25.0f), glm::vec3(0.0, 0.0, 1.0) );
            glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(model) );
            glDrawArrays(GL_TRIANGLES, 0, 3);

        // === END RENDERING ==========// 

        SDL_GL_SwapWindow(window);
    }

    std::puts(" [TRACE] Shutdown gracefully. ");
    return 0;
} 

// ----- I M P L E M E N T A T I O N S --------------// 

    // ====== I M P L E M E N T A T I O N S ==========//

void compile_shader(GLuint m_program, const char* code, GLenum type)
{
    GLint shader_id = glCreateShader( type );
    glShaderSource(shader_id, 1, &code, nullptr);
    glCompileShader(shader_id);

    GLint is_compiled = GL_FALSE;
    // Check shader compilation result
    glGetShaderiv(shader_id, GL_COMPILE_STATUS, &is_compiled);

    // If there is any shader compilation result,
    // print the error message.
    if( is_compiled == GL_FALSE)
    {
        GLint length;
        glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &length);
        assert( length > 0 );

        std::string out(length + 1, 0x00);
        GLint chars_written;
        glGetShaderInfoLog(shader_id, length, &chars_written, (char*) out.data());
        std::cerr << " [SHADER ERROR] = " << out << '\n';
        // Abort the exection of current process.
        std::abort();
    }

    glAttachShader(m_program, shader_id);
    glDeleteShader(shader_id);
    glLinkProgram(m_program);
    GLint link_status = GL_FALSE;
    glGetProgramiv(m_program, GL_LINK_STATUS, &link_status);
    assert( link_status != GL_FALSE );
    // glUseProgram(m_program);
}

// Upload buffer from main memory to GPU VBO
// =>> Parameters VAO, VBO are allocated by the caller.
void send_buffer( GLuint* pVao, GLuint* pVbo, GLsizei sizeBuffer
                , void* pBufffer, GLint   shader_attr, GLint size
                , GLenum type)
{
    assert(pVao != nullptr);
    assert(pVbo != nullptr);
    GLuint& vao = *pVao;
    GLuint& vbo = *pVbo;

    // Generate and bind current VAO (Vertex Array Object)
    if(vao == 0){ glGenVertexArrays(1, &vao); }
    glBindVertexArray(vao);

    // Generate and bind current VBO (Vertex Buffer Object)
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);

    // Upload data to current VBO buffer in GPU
    glBufferData(GL_ARRAY_BUFFER, sizeBuffer, pBufffer, GL_STATIC_DRAW);
    glEnableVertexAttribArray(shader_attr);
    glVertexAttribPointer(shader_attr, size, type, GL_FALSE, 0, nullptr);

    // ------ Disable Global state set by this function -----//
    //
    // Unbind VAO
    glBindVertexArray(0);
    // Unbind VBO
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Disable current shader attribute
    glDisableVertexAttribArray(shader_attr);

}

// ----- S H A D E R S ------------------------------// 

const char* shader_vertex = R"(
    #version 330 core

    layout ( location = 2)  in vec2 position;
    layout ( location = 1 ) in vec3 color;

    out vec3 out_color;
    uniform mat4 u_model;
    uniform mat4 u_projection;

    void main()
    {
       gl_Position = u_projection * u_model * vec4(position, 0, 1.0);
       out_color = color;
    }

 )";


const char* shader_fragment = R"( 
    #version 330

    // This color comes from Vertex shader
    in vec3 out_color;

    void main()
    {
        // Set vertex colors
        gl_FragColor = vec4(out_color, 1.0);
    }

)";

Building

Building on Linux.

$ cmkae -H. -B_build_linux -DCMAKE_BUILD_TYPE=Debug
$ cmkae --build _build_linux --target 

Building for Windows NT (cross-compilation) from Linux.

$ docker run -it --rm -v $PWD:/cwd -w /cwd --entrypoint=bash dockcross/windows-static-x64
$ cmake -H. -B_build_windows -DCMAKE_BUILD_TYPE=Debug
$ cmake --build _build_windows --target 

$ ls 
CMakeCache.txt  Makefile                 _deps                glew32.dll
CMakeFiles      SDL2ConfigVersion.cmake  cmake_install.cmake  opengl-sdl.exe

Run with wine:

$ wine _build_windows/opengl-sdl.exe 

Building with Xmake:

 $ xmake -P .
[100%]: build ok!

Running the application: (default target)

$ xmake run -P .
[TRACE] OpenGL SDL started Ok. 
[TRACE] Attribute location attr_position = 2 
[TRACE] Shutdown gracefully. 

Generate a CMake file and edit with VSCode or CLion:

$ xmake project -k cmake  -P . 
create ok!

# Edit the project with VSCode 
$ vscode . 

1.15 Minimal Qt5 OpenGL project

Qt Framework is a better fit for non-games cross-platform OpenGL graphical applications like 3D drawing akin to Blender, CAD (Computer-Aided Design), scientific visualization and charts than GLFW, SDL or SFML as those window abstraction libraries lack widgets, such as buttons, menus, text entry and so on which are necessary for rich and complex interaction. Other significant advantage of Qt over the aforementioned libraries is that the Qt framework already has everything that is needed for developing OpenGL applications, including, OpenGL loading, 4x4 matrix, quaternion, OpenGL widget and scene graph.

Documentation:

Examples:

Screenshot

In this sample application, the vertex data is uploaded only once to the GPU via VBO (Vertex Buffer Object) and used for drawing two triangles, which the positions are set via affine trasnform objects. The program has tree buttons that controls the second triangle by performing the following set of operations: scale increasing; scale decreasing and rotation.

opengl-qt3d-minimal.png

Figure 8: Minimal Qt 3D OpenGL application.

Project Files

File: CMakeLists.txt

cmake_minimum_required(VERSION 3.9)
project(Qt5_Widget_OpenGL)

#====== Global Configurations ==================#

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt5 COMPONENTS Core Widgets REQUIRED)

#=============== Target Configurations ============#
SET(QT5_LINK_LIBRARIES Qt5::Core Qt5::Gui Qt5::Widgets)

       add_executable( qt-opengl qt-opengl.cpp )
target_link_libraries( qt-opengl  Qt5::Core Qt5::Gui Qt5::Widgets )

File: meson.build (Note: poor IDE support) - For Meson Building System

project('qt-opengl-meson', 'cpp')

qt5      = import('qt5')
qt5_deps = dependency('qt5', modules: ['Core', 'Gui', 'Widgets'] )

executable( 'qt-opengl'             # Executable name without extension 
          , 'qt-opengl.cpp'         # Source files
          , dependencies: qt5_deps  # Depencies 
          ) 

File: xmake.lua - For XMake building system

add_rules("mode.debug", "mode.release")

target("qt-opengl")
    add_rules("qt.widgetapp")
    set_languages("c99", "c++1z")
    add_files("qt-opengl.cpp")

File: qt-opengl.cpp

#include <iostream>
#include <functional>
#include <cassert>
#include <cmath>

#include <QtWidgets>
#include <QApplication>
#include <QOpenGLWindow>
#include <QOpenGLContext>
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>


extern const char* shader_vertex;
extern const char* shader_fragment;

// Wrapper for 2D vertex coordinates
struct Vertex2D{  GLfloat x, y;  };
// Wrapper for RGB colors
struct ColorRGB { GLfloat r, g, b; };

struct Mesh
{
    QOpenGLBuffer*            vbo_vertex  = nullptr;
    QOpenGLBuffer*            vbo_color   = nullptr;
    QOpenGLVertexArrayObject* vao         = nullptr;
};

struct Transform
{
    QVector3D   position = {0.0,  0.0,  0.0,   };
    QVector3D   scale    = {1.0f, 1.0f, 1.0f   };
    QQuaternion rotation = {1.0, 0.0, 0.0, 0.0 };

    // Cached affine trasnform for speeding up computation
    // by tranding off time for space.
    QMatrix4x4 trasnform;

    // When this flag is true, the transform is computed again.
    bool is_changed = false;

    Transform& setPosition(float x, float y, float z)
    {
        position = {x, y, z};
        is_changed = true;
        return *this;
    }

    Transform& displace(float dx, float dy, float dz)
    {
        position = { position.x() + dx, position.y() + dy, position.z() + dz };
        is_changed = true;
        return *this;
    }

    Transform&  setRotationZ(float angle)
    {
        rotation = QQuaternion::fromAxisAndAngle( QVector3D{0.0, 0.0, 1.0}, angle );
        is_changed = true;
        return *this;
    }

    Transform& rotateZ(float angle)
    {
        rotation = rotation * QQuaternion::fromAxisAndAngle( QVector3D{0.0, 0.0, 1.0}, angle );
        is_changed = true;
        return *this;
    }

    Transform& setScale(float f)
    {
        scale = {f, f, f};
        is_changed = true;
        return *this;
    }

    Transform& addScale(float f)
    {
        scale = { scale.x() + f, scale.y() + f, scale.z() + f };
        is_changed = true;
        return *this;
    }

    QMatrix4x4 value()
    {

        if( is_changed )
        {
            std::fprintf(stderr, " [TRACE] Affine transform recalculated. Ok. \n");
            trasnform.setToIdentity();
            trasnform.translate( position );
            trasnform.scale(scale);
            trasnform.rotate(rotation);
            is_changed = false;
        }
        return trasnform;
    }
};

class OpenGLCanvas: public QOpenGLWidget
{
public:
     using DrawCallback = std::function<void (QOpenGLExtraFunctions* ctx)>;
private:
    QOpenGLShaderProgram* m_prog    = nullptr;


    // Shader attribute location
    int attr_position = 0;
    int attr_color    = 0;
    // Shader uniform locations
    int u_model = 0;
    int u_proj  = 0;

    DrawCallback m_callback = [](QOpenGLExtraFunctions* ){ };
    DrawCallback m_init     = [](QOpenGLExtraFunctions* ){ };
public:

    OpenGLCanvas(DrawCallback init):  m_init(init){ }

    void initializeGL() override
    {
        auto ctx = QOpenGLContext::currentContext()->extraFunctions();
        std::fprintf(stderr, " [TRACE] Initialized Ok. \n");

        m_prog = new QOpenGLShaderProgram;
        m_prog->addShaderFromSourceCode(QOpenGLShader::Vertex,   shader_vertex);
        m_prog->addShaderFromSourceCode(QOpenGLShader::Fragment, shader_fragment);
        m_prog->link();
        m_prog->bind();
        // Attribute location
        attr_position = m_prog->attributeLocation("position");
        Q_ASSERT( attr_position != -1 );
        attr_color = m_prog->attributeLocation("color");
        Q_ASSERT( attr_color != -1    );

        // Uniform variables
        u_model = m_prog->uniformLocation("u_model");
        Q_ASSERT( u_model != -1 );
        u_proj  = m_prog->uniformLocation("u_proj");
        Q_ASSERT( u_proj != -1 );

        // Initialize shader uniform variables
        QMatrix4x4 m;
        m.setToIdentity();
        m_prog->setUniformValue(u_model, m);
        m_prog->setUniformValue(u_proj,  m);

        m_init(ctx);
   }

    void setDrawCallback(DrawCallback callback)
    {
        m_callback = callback;
    }

    // Override QOpenGLWidget::paintGL()
    void paintGL() override
    {
        std::fprintf(stderr, " [TRACE] Painting window Ok. \n");

        QOpenGLExtraFunctions* ctx = QOpenGLContext::currentContext()->extraFunctions();
        ctx->glClearColor(0.0, 0.0, 0.0, 1.0);
        ctx->glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

        m_callback(ctx);
    }

    // Override QOpenGLWidget::resizeGL()
    void resizeGL(int w, int h) override
    {
        std::fprintf(stderr, " [TRACE] Window resized. \n");

        // Forward call to superclass
        this->QOpenGLWidget::resizeGL(w, h);
    }

    // Set model matrix of current object
    void setModel(QMatrix4x4 const& m){ m_prog->setUniformValue(u_model, m); }

    // Upload vertices to GPU
    Mesh upload(size_t num_vertices, Vertex2D* vertices, ColorRGB* colors)
    {
        auto ctx = QOpenGLContext::currentContext()->extraFunctions();

        auto vao = new QOpenGLVertexArrayObject;
        if( vao->create() ){ vao->bind(); }

        // Send vertices positions to GPU
        auto vbo_vertex = new QOpenGLBuffer;
        vbo_vertex->create();
        vbo_vertex->bind();
        vbo_vertex->allocate( vertices , num_vertices * sizeof(Vertex2D) );
        ctx->glEnableVertexAttribArray(attr_position);
        // Set data layout, how the data will be interpreted.
        ctx->glVertexAttribPointer( attr_position, 2, GL_FLOAT, GL_FALSE, 0, nullptr );

        // Send vertices colors to GPU
        auto vbo_color = new QOpenGLBuffer;
        vbo_color->create();
        vbo_color->bind();
        vbo_color->allocate( colors, num_vertices * sizeof(ColorRGB));
        ctx->glEnableVertexAttribArray(attr_color);
        // Set color data layout
        ctx->glVertexAttribPointer( attr_color, 3, GL_FLOAT, GL_FALSE, 0, nullptr );

        Mesh mesh;
        mesh.vbo_vertex = vbo_vertex;
        mesh.vbo_color  = vbo_color;
        mesh.vao = vao;
        return mesh;
    }
};


int main(int argc, char** argv)
{
    std::cout << " [INFO] Starting Application" << std::endl;

    QApplication app(argc, argv);
    app.setApplicationName("Qt OpenGL Cavans");

    constexpr size_t NUM_VERTICES = 3;

    const float a = 0.5f ;

    Vertex2D vertices[NUM_VERTICES] = {
          {  0.0,  a * sqrtf(3.0f) } // Vertex 0
        , {   -a,  0.0             } // Vertex 1
        , {    a,  0.0             } // Vertex 2
    };

    ColorRGB colors[NUM_VERTICES] = {
          {0.4, 0.5, 0.67}
        , {0.8, 0.6, 0.30}
        , {0.2, 0.7, 0.10}
    };


    QWidget window;
    window.resize(500, 400);
    window.setWindowTitle("Qt OpenGL Window is awesome!");

    auto hbox = new QVBoxLayout(&window);

    auto form = new QHBoxLayout();
    auto btn_add_scale = new QPushButton("Increase scale");
    auto btn_dec_scale = new QPushButton("Decrease scale");
    auto btn_rotate = new QPushButton("Rotate");
    form->addWidget(btn_add_scale);
    form->addWidget(btn_dec_scale);
    form->addWidget(btn_rotate);
    hbox->addLayout(form);

    Mesh mesh_triangle;

    Transform trf_triangle1;
    trf_triangle1.setPosition(0.36f, 0.50f, 0.0f)
                 .rotateZ(0)
                 .setScale(0.50);


    Transform trf_triangle2;
    trf_triangle2.setPosition(-0.24, -0.25, 0.0)
                 .setScale(0.80)
                 .rotateZ(35.0);

    qDebug() << " Trasnform 1 = " << trf_triangle1.value();
    qDebug() << " Transform 2 = " << trf_triangle2.value();

    OpenGLCanvas* canvas = new OpenGLCanvas([&](QOpenGLExtraFunctions* ctx)
    {
        mesh_triangle = canvas->upload(NUM_VERTICES, vertices, colors);
    });
    canvas->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    hbox->addWidget(canvas);

    canvas->setDrawCallback([&](QOpenGLExtraFunctions* ctx)
    {
        // Draw triangle 1
        mesh_triangle.vao->bind();
        // canvas->setModel(model_triangle1);
        canvas->setModel( trf_triangle1.value() );
        ctx->glDrawArrays(GL_TRIANGLES, 0, NUM_VERTICES);

        // Draw triangle 2
        mesh_triangle.vao->bind();
        // canvas->setModel(model_triangle2);
        canvas->setModel( trf_triangle2.value() );
        ctx->glDrawArrays(GL_TRIANGLES, 0, NUM_VERTICES);
    });

    window.show();


    // ----------- Set events ----------------------//

    QObject::connect(btn_add_scale, &QPushButton::clicked,
             [&]{
                   std::fprintf(stderr, " [TRACE] Button to increase scale clicked. Ok. \n"); 
                   trf_triangle2.addScale(+0.10);
                   canvas->repaint();
             });

    QObject::connect(btn_dec_scale, &QPushButton::clicked,
                     [&]{
                        std::fprintf(stderr, " [TRACE] Button to decrease scale clicked. Ok. \n"); 
                        trf_triangle2.addScale(-0.10);
                        canvas->repaint();
                     });

    QObject::connect(btn_rotate, &QPushButton::clicked,
                     [&]{
                         std::fprintf(stderr, " [TRACE] Button to rotate triangle clicked. Ok. \n");
                         trf_triangle2.rotateZ(10.5);
                         canvas->repaint();
                     });

    return app.exec();
}


// ===== S H A D E R - P R O G R A M S ==========//


const char* shader_vertex = R"(
    #version 330 core

    layout ( location = 2)  in vec2 position;
    layout ( location = 1 ) in vec3 color;

    out vec3 out_color;
    uniform mat4 u_model;
    uniform mat4 u_proj;

    void main()
    {
       gl_Position = u_proj * u_model * vec4(position, 0, 1.0);
       out_color = color;
    }

 )";


const char* shader_fragment = R"(
    #version 330

    // This color comes from Vertex shader
    in vec3 out_color;

    void main()
    {
        // Set vertex colors
        gl_FragColor = vec4(out_color, 1.0);
    }

)";

Building with CMake

 # Build application 
 $ cmake -H. -B_build -DCMAKE_BUILD_TYPE=Debug
 $ cmake --build _build --target all

 # List build tree directory 
 $ >> ls _build/
CMakeCache.txt  cmake_install.cmake  qt-opengl*
CMakeFiles/     Makefile             qt-opengl_autogen/

# Run application 
$ >> _build/qt-opengl 

Building with Meson

# Build application 
$ meson setup _build_meson # Name of build directory is arbitrary
$ meson compile -C _build_meson 

# List build directory 
$ >> ls _build_meson/
build.ninja            meson-info/  meson-private/  qt-opengl.p/
compile_commands.json  meson-logs/  qt-opengl* 

# Run application 
$ >> ./_build_meson/qt-opengl

Building with XMake

Build project with Xmake lua-based building system showing all compilation and linking process.

$  xmake build -v

Run application (default target):

$ xmake run 

Run application specifying the target:

$ xmake run qt-opengl 

Generate an CMakeLists.txt for making IDEs and smart text editors including, CLion, QtCreator and Visual Studio Code happy.

$ >> xmake project -k cmake
create ok!

# Edit project with Visual Studio Code 
$ code . 

# Edit project with CLion 
=>> Open the project directory with CLion 

# Edit project with QtCreator 
=>> Open the project directory with Qt creator 
$ qt-creator . 

# Build project with CMakeLists.txt 
$ cmake -H. -B_build -DCMAKE_BUILD_TYPE=Debug
$ cmake --build _build --target 

1.16 Minimal WebGL-OpenGL project

WebGL is a web standard supported by most modern web browsers for a low-level 3D graphics API accelerated by GPU based on OpenGL ES. By using WebGL as in this next code, it is possible to play with OpenGL 3D computer graphics without the hassle of dealing with C or C++ compilation and building systems since WebGL API is already available in any web browser and the API is exposed as JavaScript API, that resembles OpenGL C API.

opengl-webgl-basic.png

Complete Html Code

<!DOCTYPE html>
<html> 
   <head> 
       <title> Web Page Title </title>
       <script src="https://somehost.com/jequey.js"></script>

   </head>

   <body>
       <h1>WebGL Experiment 1 - Draw a triangle</h1>
       <canvas id="glCanvas" width="480" height="480">
   </body>
  <script>
    // Source: https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html
    function createShader(gl, type, source) 

      ... ... ... ... ... ... ... ... ... ... .. 
      ... ... ... ... ... ... ... ... ... ... ... 

    // Draw Square 
    console.log(" [TRACE] Draw sqaure in WebGL");
    gl.bindBuffer( gl.ARRAY_BUFFER, vbo_square );
    gl.vertexAttribPointer(attr_position, 2, gl.FLOAT, false, 0, 0);
    gl.drawArrays(gl.LINE_LOOP, 0, 4);

    console.log(" [TRACE] Loaded Ok.");
  </script>

</html> 

JavaScript part:

// Source: https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html
function createShader(gl, type, source) 
{
  var shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (success) {
    return shader;
  }

  console.log(gl.getShaderInfoLog(shader));
  gl.deleteShader(shader);
}

// Source:  https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html
function createProgram(gl, vertexShader, fragmentShader) 
{
  var program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  var success = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (success) {
    return program;
  }

  console.log(gl.getProgramInfoLog(program));
  gl.deleteProgram(program);
}


function resizeCanvasToDisplaySize(canvas, multiplier) 
{
    multiplier = multiplier || 1;
    const width  = canvas.clientWidth  * multiplier | 0;
    const height = canvas.clientHeight * multiplier | 0;
    if (canvas.width !== width ||  canvas.height !== height) {
      canvas.width  = width;
      canvas.height = height;
      return true;
    }
    return false;
  }


const canvas = document.querySelector("#glCanvas");
const gl = canvas.getContext("webgl");

if(  gl == null ){
  alert(" [ABORT] Unable to initialized WebGL");
}

// Vertex Shader code => Performs vertex coordinate transforms in the GPU 
// Defines the a 3D scene or 2D scene (by ignoring the Z axis).
let code_shader_vertex = `
  attribute vec2 position;

  void main(){
     // Coordinates: X, Y, Z = 0, W = 1 => Ignores the Z axis (2D Scene)
     gl_Position = vec4(position, 0, 1.0);

  }
`;

// Fragment shader code => Sets color, lights and illumination 
let code_shader_fragment = `
  void main(){
    // Color always green 
    // Color in (R, G, B, W) => (Red, Green, Blue, W)
    gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
  }
`;

// Vertices of (x, y) coordinates 
let vertices_triangle = [
    -0.25, -0.25
  ,  0.00,  0.25
  ,  0.25, -0.25
];

let vertices_square = [
    -0.3, -0.3
  , -0.3,  0.3
  ,  0.3,  0.3
  ,  0.3, -0.3

];


let shader_vert = createShader(gl, gl.VERTEX_SHADER, code_shader_vertex);
let shader_frag = createShader(gl, gl.FRAGMENT_SHADER, code_shader_fragment);
let prog = createProgram(gl, shader_vert, shader_frag);

let attr_position = gl.getAttribLocation(prog, "position");
console.log(" [TRACE] attr_position = ", attr_position);

// resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

// Screen background color is black => clearColor(Red intensity from 0 to 1, Green , Blue , Alpha)
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(prog);

// -------- Upload triangle to GPU --------------------//
// GLuint vbo_triangle_ = 0;
// glGenBuffers(1 , &vbo_triangle_); 
let vbo_triangle = gl.createBuffer();
// Set this buffer as current buffer (global state)
gl.bindBuffer( gl.ARRAY_BUFFER, vbo_triangle );
// Upload vertices to  GPU 
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices_triangle), gl.STATIC_DRAW);

// --------- Upload squares to GPU ----------------------//
let vbo_square = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, vbo_square );
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices_square), gl.STATIC_DRAW);

// ------ Drawing ----------------------------------------//
gl.enableVertexAttribArray(attr_position);

// Draw triangle 
console.log(" [TRACE] Draw triangle in WebGL");
gl.bindBuffer( gl.ARRAY_BUFFER, vbo_triangle );
gl.vertexAttribPointer(attr_position, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 3);

// Draw Square 
console.log(" [TRACE] Draw sqaure in WebGL");
gl.bindBuffer( gl.ARRAY_BUFFER, vbo_square );
gl.vertexAttribPointer(attr_position, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.LINE_LOOP, 0, 4);

console.log(" [TRACE] Loaded Ok.");

1.17 Loading OpenGL without Glew

The next code loads OpenGL function pointers without Glew (OpenGL extension wrangler) for streamlining the building process. Glew is a library for loading OpenGL function pointers and OpenGL extensions in a platform-agnostic way. Despite the usefulness of this library, its source code is hard to integrate to building systems as the Glew building systems as Glew library needs Perl and other dependencies for code generation.

Relevant documentation:

Screenshot

opengl-load-function-ptr.png

Files

File: CMakeListst.txt

cmake_minimum_required(VERSION 3.5)
project(OpenGL_Load_FPTR)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)

# Set GLFW Options before FectchContent_MakeAvailable
set( GLFW_BUILD_EXAMPLES OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_TESTS    OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_DOCS     OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_INSTALL  OFF CACHE BOOL  "GLFW lib only" )

# Donwload GLFW library
FetchContent_Declare(
  glfwlib
  URL   https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
)
FetchContent_MakeAvailable(glfwlib)

MACRO(ADD_OPENGL_APP target sources)
   add_executable( ${target} ${sources} )
   message([TRACE] " Add OpenGL executable = ${target} ")

   target_link_libraries( ${target} glfw OpenGL::GL)

    IF(MINGW)
        # Statically link against MINGW dependencies
        # for making easier to deploy on other machines. 
        target_link_options( ${target} PRIVATE                                 
                                 -static-libgcc
                                 -static-libstdc++
                                 -Wl,-Bstatic,--whole-archive -lwinpthread
                                   -Wl,--no-whole-archive                                    
                                 )
     ENDIF()       

ENDMACRO()     

     # ======= T A R G E T S ============================#
     #                                                   #

ADD_OPENGL_APP( load-opengl-fptr  load-opengl-fptr.cpp )

File: load-openg-fptr.cpp

// ----- Manual Loading of OpenGL function pointers -------//
#include <iostream>
#include <vector>
#include <array>
#include <cmath>
#include <cassert>

// -------- OpenGL headers ---------//
//
#define GL_GLEXT_PROTOTYPES 1 // Necessary for OpenGL >= 3.0 functions
#define GL3_PROTOTYPES      1 // Necessary for OpenGL >= 3.0 functions


#if defined(_WIN32)
   #include <windows.h>
#endif

#include <GL/gl.h>

#if defined(__linux__) || defined(__bsd__)
    #include <GL/glx.h>  // For glXGetProcAddress() and glXGetProcAddressARB()
    #include <GL/glxext.h>
#endif

#include <GL/glu.h>
#include <GLFW/glfw3.h>


//======== B O I L E R P L A T E - F O R - L O A D I N G - O P E N G L ==========//
//

#define GL_ARRAY_BUFFER  0x8892
#define GL_STATIC_DRAW   0x88E4

// Generic void function pointer which can be casted to any other function pointer.
using FptrVoid = void*;

/** @brief Load OpenGL function pointer at runtime
  * See:
  *    -
  *    - https://stackoverflow.com/questions/21769427/wglgetprocaddress-returns-null
  */
FptrVoid load_gl_function(const char* name)
{
    FptrVoid fp = nullptr;
    std::fprintf(stderr, " [TRACE] Loading pointer to OpenGL subroutine = %s \n", name);

    #if defined(_WIN32)
        assert( wglGetCurrentContext() != nullptr && "Current thread lacks a OpenGL rendering context." );

        fp = (void*) wglGetProcAddress(name);
        // assert( fp != nullptr );
        if( fp == 0
            || (fp == (void*)0x1) || (fp == (void*)0x2) || (fp == (void*)0x3)
            || (fp == (void*)-1) )
          {
            HMODULE module = LoadLibraryA("opengl32.dll");
            fp = (void *) GetProcAddress(module, name);
          }

        if( fp == nullptr)
        {
            std::fprintf(stderr, " [ERROR] OpenGL function = '%s' not found on Windows ", name );
            std::abort();
        }
    #elif defined(__linux__) || defined(__bsd__)
        fp = (FptrVoid) glXGetProcAddressARB( (const unsigned char*) name);

        if( fp == nullptr )
        {
            std::fprintf(stderr, " [ERROR] OpenGL function not found on Linux or BSD " );
            std::abort();
        }
    #endif
    return fp;
}

typedef unsigned int    GLenum;
typedef unsigned char   GLboolean;
typedef unsigned int    GLbitfield;
typedef void        GLvoid;
typedef signed char GLbyte;     /* 1-byte signed */
typedef short       GLshort;    /* 2-byte signed */
typedef int     GLint;      /* 4-byte signed */
typedef unsigned char   GLubyte;    /* 1-byte unsigned */
typedef unsigned short  GLushort;   /* 2-byte unsigned */
typedef unsigned int    GLuint;     /* 4-byte unsigned */
typedef int     GLsizei;    /* 4-byte signed */
typedef float       GLfloat;    /* single precision float */
typedef float       GLclampf;   /* single precision float in [0,1] */
typedef double  GLdouble;   /* double precision float */
typedef double  GLclampd;   /* double precision float in [0,1] */

#ifdef _WIN64
    typedef signed   long long int khronos_intptr_t;
    typedef unsigned long long int khronos_uintptr_t;
    typedef signed   long long int khronos_ssize_t;
    typedef unsigned long long int khronos_usize_t;
#else
    typedef signed   long  int     khronos_intptr_t;
    typedef unsigned long  int     khronos_uintptr_t;
    typedef signed   long  int     khronos_ssize_t;
    typedef unsigned long  int     khronos_usize_t;
#endif


typedef khronos_ssize_t GLsizeiptr;
typedef khronos_intptr_t GLintptr;

using glClear_t = void (*) ( unsigned int mask );
using glClearColor_t = void (*) ( float red, float green, float blue, float alpha );
using glBindBuffer_t = void (*) (GLenum target, GLuint buffer);
using glGenBuffers_t = void (*) (GLsizei n, GLuint * buffers);
using glBufferData_t = void (*) (GLenum target, GLsizeiptr size, const void *data, GLenum usage);
using glEnableVertexAttribArray_t = void (*) (GLuint index);
using glDisableVertexAttribArray_t = void (*) (GLuint index);
using glVertexAttribPointer_t = void (*) (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer);
using glDrawArrays_t = void (*) ( GLenum mode, GLint first, GLsizei count );

struct OpenGL {

    OpenGL() {
        this->glClear = reinterpret_cast<glClear_t>( load_gl_function("glClear") );
        // It should be used reinterpret_cast<>, but C-style cast is used for breviety.
        this->glClearColor = (glClearColor_t) load_gl_function("glClearColor");
        this->glBindBuffer = (glBindBuffer_t) load_gl_function("glBindBuffer");
        this->glGenBuffers = (glGenBuffers_t) load_gl_function("glGenBuffers");
        this->glBufferData = (glBufferData_t) load_gl_function("glBufferData");
        this->glEnableVertexAttribArray   = (glEnableVertexAttribArray_t) load_gl_function("glEnableVertexAttribArray");
        this->glDisableVertexAttribArray  = (glDisableVertexAttribArray_t) load_gl_function("glDisableVertexAttribArray");
        this->glVertexAttribPointer       = (glVertexAttribPointer_t) load_gl_function("glVertexAttribPointer");
        this->glDrawArrays                = (glDrawArrays_t) load_gl_function("glDrawArrays");

    }

    glClear_t               glClear;
    glClearColor_t                glClearColor;
    glBindBuffer_t              glBindBuffer;
    glGenBuffers_t              glGenBuffers;
    glBufferData_t                  glBufferData;
    glEnableVertexAttribArray_t   glEnableVertexAttribArray;
    glDisableVertexAttribArray_t  glDisableVertexAttribArray;
    glVertexAttribPointer_t         glVertexAttribPointer;
    glDrawArrays_t                glDrawArrays;
};

// ====== S T A R T - O F - A P P L I C A T I O N - C O D E ============//
//

struct Vertex2D
{
    GLfloat x;
    GLfloat y;
};

int main(int argc, char** argv)
{


    GLFWwindow* window;

    // Initialize GLFW 
    if (!glfwInit()){ return -1; }

    std::fprintf(stderr, " [TRACE] GLFW initialized Ok. \n");


    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(640, 480, "Load OpenGL functiion pointers", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    } 

    // ----- Before defining current OpenGL context ----//
        glfwMakeContextCurrent(window);
    // ----- After defining current OpenGL context -----//

    // GL object Must only be instantiated after an OpenGL context (Window) is obtained
    OpenGL gl;

    gl.glClearColor(0.0f, 0.5f, 0.6f, 1.0f);

    Vertex2D triangle_points[3] = {
        Vertex2D{-0.25,   -0.25}
      , Vertex2D{ 0.00,   0.25}
      , Vertex2D{ 0.25,  -0.25}
    };

    Vertex2D square_points[4] = {
        { -0.3,  -0.3 }
      , { -0.3,   0.3 }
      , {  0.3,   0.3 }
      , {  0.3,  -0.3 }
    };

    // ================== Triangle Buffer ====================//
    //

    GLuint vbo_triangle_ = 0;

    // Create an OpenGL VBO buffer
    gl.glGenBuffers(1, &vbo_triangle_ );
    assert( vbo_triangle_ != 0 );
    gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_);
    gl.glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_points), triangle_points, GL_STATIC_DRAW);
    gl.glBindBuffer(GL_ARRAY_BUFFER, 0);

    // ================ Square / Vertex Buffer Object 2 ==================//
    //

    GLuint vbo_square_ = 0;
    gl.glGenBuffers(1, &vbo_square_);
    // Check for error
    assert( vbo_square_ != 0);
    gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_square_);
    gl.glBufferData(GL_ARRAY_BUFFER, sizeof(square_points), square_points, GL_STATIC_DRAW);
    gl.glBindBuffer(GL_ARRAY_BUFFER, 0);

    GLint shader_attr = 0;

    //  ======= R E N D E R  - L O O P ============//
    //
    while ( !glfwWindowShouldClose(window) )
    {
            gl.glClear(GL_COLOR_BUFFER_BIT);

        // ====== BEGIN RENDERING ============//

            // ------------ Draw triangle --------------//
            //
            gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_);
            gl.glEnableVertexAttribArray(shader_attr);

            gl.glVertexAttribPointer(shader_attr, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
            glDrawArrays(GL_TRIANGLES, 0, 3);

            // Disable global state
            gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
            gl.glDisableVertexAttribArray(0);

            //------------ Draw Square -------------------------//
            //

            #if 1
            gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_square_);
            gl.glEnableVertexAttribArray(shader_attr);
            gl.glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
            // Plot 4 vertices
            gl.glDrawArrays(GL_LINE_LOOP, 0, 4);

            // Disable global state
            gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
            gl.glDisableVertexAttribArray(shader_attr);
            #endif

        // ====== END RENDERING ==============//

        /* Swap front and back buffers */
        glfwSwapBuffers(window);
        /* Poll for and process events */
        glfwPollEvents();

        if( glfwGetKey(window, 'Q' ) == GLFW_PRESS )
        {
             std::cout << " [TRACE] User typed Q =>> Shutdown program. Ok. " << '\n';
             break;
        }
    }

    glfwTerminate();
    return 0;
}

Building

Building on Linux:

$ cmake -H. -B_build_linux -DCMAKE_BUILD_TYPE=Debug 
$ cmake --build _build_linux --target

Building for Windows with Dockercross tool:

$ docker run -it --rm -v $PWD:/cwd -w /cwd --entrypoint=bash dockcross/windows-static-x64
$ cmake -H. -B_build_cross -DCMAKE_BUILD_TYPE=Debug 
$ cmake --build _build_cross --target

$ wine _build_cross/load-opengl-fptr.exe 
 [TRACE] GLFW initialized Ok. 
 [TRACE] Loading pointer to OpenGL subroutine = glClear 
 [TRACE] Loading pointer to OpenGL subroutine = glClearColor 
 [TRACE] Loading pointer to OpenGL subroutine = glBindBuffer 
 [TRACE] Loading pointer to OpenGL subroutine = glGenBuffers 
 [TRACE] Loading pointer to OpenGL subroutine = glBufferData 
 [TRACE] Loading pointer to OpenGL subroutine = glEnableVertexAttribArray 
 [TRACE] Loading pointer to OpenGL subroutine = glDisableVertexAttribArray 
 [TRACE] Loading pointer to OpenGL subroutine = glVertexAttribPointer 
 [TRACE] Loading pointer to OpenGL subroutine = glDrawArrays 

1.18 2D - using VAO

A VAO (Vertex Array Object) is an object that stores all the information necessary to render a VBO (Vertex Buffer Object). For instance, a VAO allows rendering the data, by performing the following set of operations only once: 1 - binding the VAO; 2 - binding the buffer via glBindBuffer() call; 3 - enabling the vertex attribute via glEnableVertexAttribArray(); 4 - setting the data layout by calling glVertexAttribPointer() subroutine. Without a VAO, all those subroutines would need to be called on every frame rendering.

Algorithm for VAO usage:

  • Setup
// Shader attribute location
GLint shader_attr = 1;

// -------------- Setup VAO --------------------//
//
GLuint vao;
glGenVertexArrays(1, &vao); // Instantiate VAO

GLuint vbo;
glGenBuffers(1, &vbo);
// Bind VBO (Recorded by VAO)
glBindBuffer(GL_ARRAY_BUFFER, vbo);

// Upload data to GPU
glBufferData(GL_ARRAY_BUFFER, buffer_size, buffer, GL_STATIC_DRAW);

glEnableVertexAttribArray(shader_attr);

// Set buffer data layout (Recorded by VAO)
glVertexAttribPointer(shader_attr, size, type, GL_FALSE, 0, nullptr);

// ------- Disable Global state ---------//

// Unbind VAO
glBindVertexArray(0);
// Unbind VBO
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// Disable current shader attribute
glDisableVertexAttribArray(shader_attr);
  • Rendering loop
     ... ...         ... ...         ... ...
     ... ...         ... ...         ... ...

//  ======= R E N D E R  - L O O P ============//
//                                             //
while ( !glfwWindowShouldClose(window) )
{
    glClear(GL_COLOR_BUFFER_BIT);
     ... ...         ... ...         ... ...
     ... ...         ... ...         ... ...

     glBindVertexArray(vao);
     glDrawArrays(GL_LINE_LOOP, 0, NUMBER_OF_VERTICES);

     glBindVertexArray(vao1);
     glDrawArrays(GL_LINE_TRIANGLES, 0, NUMBER_OF_VERTICES_1);

     glBindVertexArray(vao2);
     glDrawArrays(GL_LINES, 0, NUMBER_OF_VERTICES_2);

     ... ...         ... ...         ... ...
     ... ...         ... ...         ... ...

    /* Swap front and back buffers */
    glfwSwapBuffers(window);
    /* Poll for and process events */
    glfwPollEvents();
}

Further Reading

Screenshot

opengl-draw2-vao.png

Figure 11: 2D OpenGL draw using VAO - Vertex Array Object

Sample Code

File: CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(draw2d)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)

# Set GLFW Options before FectchContent_MakeAvailable
set( GLFW_BUILD_EXAMPLES OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_TESTS    OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_DOCS     OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_INSTALL  OFF CACHE BOOL  "GLFW lib only" )

# Donwload GLFW library
FetchContent_Declare(
  glfwlib
  URL   https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
)
FetchContent_MakeAvailable(glfwlib)

       add_executable( draw2d-vao  draw2d-vao.cpp )
target_link_libraries( draw2d-vao  glfw OpenGL::GL GLU)

File: draw2d-vao.cpp

// Draw curve mapping buffer to the GPU.
#include <iostream>
#include <vector>
#include <array>
#include <cmath>
#include <cassert>

// -------- OpenGL headers ---------//
//
#define GL_GLEXT_PROTOTYPES 1 // Necessary for OpenGL >= 3.0 functions
#define GL3_PROTOTYPES      1 // Necessary for OpenGL >= 3.0 functions

#include <GL/gl.h>
#include <GLFW/glfw3.h>

// #include <GL/glew.h>
#include <GL/glu.h>
#include <GL/glut.h>


struct Vertex2D
{
    GLfloat x;
    GLfloat y;
};

// Upload buffer from main memory to GPU VBO
// =>> Parameters VAO, VBO are allocated by the caller.
void send_buffer(   GLuint* pVao        // Pointer to VAO (Vertex Array Object) - allocated by caller
                  , GLuint* pVbo        // Pointer to VBO (Vertex Buffer Object) - allocated by caller
                  , GLsizei sizeBuffer  // Total buffer size in bytes
                  , void*   pBufffer    // Pointer to buffer
                  , GLint   shader_attr // Shader attribute location
                  , GLint   size        // Number of coordinates of a given vertex
                  , GLenum  type        // Type of each element coordinate
                  );

void* map_buffer( GLuint* pVao         // Pointer to VAO - allocated by calling code.
                , GLuint* pVbo         // Pointer to VBO - allocated by calling code.
                , GLsizei data_size    // Total size in bytes that is allocated to buffer
                , GLint   shader_attr  // Shadder attribute location id (0 - zero) is there is no shader.
                , GLint   size         // Number of component of each vertex
                , GLenum  type         // Type of each vertex element
                );


int main(int argc, char** argv)
{
    GLFWwindow* window;

    /* Initialize the library */
    if (!glfwInit())
        return -1;

    glutInit(&argc, argv);

    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(640, 480, "2D Drawing using VAO", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);
    // glClearColor(0.0f, 0.5f, 0.6f, 1.0f);
    glClearColor(0.0f, 0.0f, 0.0, 1.0f);

    // Shader attribute location (default 0)
    GLint shader_attr = 0;


    // ================ Square / Vertex Buffer Object 2 ==================//
    //

    Vertex2D square_points[4] = {
        { -0.3,  -0.3 }
      , { -0.3,   0.3 }
      , {  0.3,   0.3 }
      , {  0.3,  -0.3 }
    };

    GLuint vao_square = 0;
    GLuint vbo_square = 0;

    // Upload square_points buffer data to GPU
    send_buffer( &vao_square, &vbo_square, sizeof(square_points)
                , square_points, shader_attr, 2, GL_FLOAT );

    assert( vao_square != 0 );
    assert( vbo_square != 0 );

    std::fprintf( stderr, " [TRACE] vao_square = %d, vbo_square = %d \n"
                 , vao_square, vbo_square);


    // ============== Circle / Vertex Buffer Object 3 ==================//
    //
    GLuint vao_circle = 0;
    GLuint vbo_circle = 0;

    // Number of vertices to used to render the circle.
    constexpr size_t N_CIRCLE = 100;
    constexpr float  PI       = 3.1415927;
    const     float  radius   = 0.75;

    // C-style cast should not be used here
    Vertex2D* pCursor = (Vertex2D*) map_buffer( &vao_circle, &vbo_circle, N_CIRCLE * sizeof(Vertex2D)
                                              , shader_attr, 2, GL_FLOAT );

    float angle = 0.0;
    const float step = 2 * PI / N_CIRCLE;

    for(size_t n = 0; n < N_CIRCLE; n++)
    {
        pCursor[n].x = radius * cosf( angle );
        pCursor[n].y = radius * sinf( angle );
        angle = angle + step;
    }


    //  ======= R E N D E R  - L O O P ============//
    //                                             //
    while ( !glfwWindowShouldClose(window) )
    {
        glClear(GL_COLOR_BUFFER_BIT);

        // ====== BEGIN RENDERING ============//

            //------------ Draw Square -------------------------//
            //
             glBindVertexArray(vao_square);
            // Plot 4 vertices
            glDrawArrays(GL_LINE_LOOP, 0, 4);
            // Unbind vao (restore global state to default value)
            glBindVertexArray(0);

            //------------ Draw Circle -------------------------//
            //
            glBindVertexArray(vao_circle);
            glDrawArrays(GL_LINE_LOOP, 0, N_CIRCLE);
            glBindVertexArray(0);

        // ====== END RENDERING ==============//

        /* Swap front and back buffers */
        glfwSwapBuffers(window);
        /* Poll for and process events */
        glfwPollEvents();

        if( glfwGetKey(window, 'Q' ) == GLFW_PRESS )
        {
             std::cout << " [TRACE] User typed Q =>> Shutdown program. Ok. " << '\n';
             break;
        }
    }

    // ---------- Dispose Buffer Objects ---------//
    glDeleteBuffers(1, &vbo_square);
    glDeleteBuffers(1, &vbo_circle);

    glfwTerminate();
    return 0;
}

// ---------- I M P L E M E N T A T I O N S ------------------//
//                                                            //

// Upload buffer from main memory to GPU VBO
// =>> Parameters VAO, VBO are allocated by the caller.
void send_buffer(   GLuint* pVao        // Pointer to VAO (Vertex Array Object) - allocated by caller
                  , GLuint* pVbo        // Pointer to VBO (Vertex Buffer Object) - allocated by caller
                  , GLsizei sizeBuffer  // Total buffer size in bytes
                  , void*   pBufffer    // Pointer to buffer
                  , GLint   shader_attr // Shader attribute location id
                  , GLint   size        // Number of coordinates of a given vertex
                  , GLenum  type        // Type of each element coordinate
                  )
{
    assert(pVao != nullptr);
    assert(pVbo != nullptr);
    GLuint& vao = *pVao;
    GLuint& vbo = *pVbo;

    // Generate and bind current VAO (Vertex Array Object)
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    // Generate and bind current VBO (Vertex Buffer Object)
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);

    // Upload data to current VBO buffer in GPU
    glBufferData(GL_ARRAY_BUFFER, sizeBuffer, pBufffer, GL_STATIC_DRAW);

    glEnableVertexAttribArray(shader_attr);

    // Set data layout - how data will be interpreted.
    glVertexAttribPointer(shader_attr, size, type, GL_FALSE, 0, nullptr);

    // ------ Disable Global state set by this function -----//
    //
    // Unbind VAO
    glBindVertexArray(0);
    // Unbind VBO
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Disable current shader attribute
    glDisableVertexAttribArray(shader_attr);

}

void* map_buffer( GLuint* pVao         // Pointer to VAO - allocated by calling code.
                , GLuint* pVbo         // Pointer to VBO - allocated by calling code.
                , GLsizei data_size    // Total size in bytes that is allocated to buffer
                , GLint   shader_attr  // Shadder attribute location id (0 - zero) is there is no shader.
                , GLint   size         // Number of component of each vertex
                , GLenum  type         // Type of each vertex element
                )
{
    GLuint& vao = *pVao;
    GLuint& vbo = *pVbo;

    // Generate and bind current VAO (Vertex Array Object)
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    // Generate and bind current VBO (Vertex Buffer Object)
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);

    /** See: https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glMapBuffer.xhtml
     *  Khronos Group: "glMapBuffer and glMapNamedBuffer map the entire data store of
     *  a specified buffer object into the client's address space. The data can then be directly
     *  read and/or written relative to the returned pointer,
     *  depending on the specified access policy."
     */
    glBufferData(GL_ARRAY_BUFFER, data_size, nullptr, GL_STATIC_DRAW);
    void* pbuffer = glMapBuffer( GL_ARRAY_BUFFER, GL_WRITE_ONLY );

    glEnableVertexAttribArray(shader_attr);
    // Set data layout - how data will be interpreted.
    glVertexAttribPointer(shader_attr, size, type, GL_FALSE, 0, nullptr);

    // ------ Disable Global state set by this function -----//
    //
    // Unbind VAO
    glBindVertexArray(0);
    // Unbind VBO
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Disable current shader attribute
    glDisableVertexAttribArray(shader_attr);

    // ------- Return --------------------------------------//
    return pbuffer;
}

1.19 2D - using shaders

OpenGL retained mode (programmable pipeline) uses shaders, which are programs that uses GLSL (OpenGL shading language) programming language, runs on the GPU (Graphics Processing Unit). A vertex shader performs vertex coordinate transformations on all vertices and colors received from VBO - Vertex Buffer Objects. The fragment shader runs after the vertex shader and deals with vertice texture and color computations.

A vertex shader has the following features:

  • Data types:
    • float => Float IEEE754 32 bits floating point scalar.
    • vec2 => Vector of 2 coordinates
    • vec3 => Vector of 3 coordinates
    • vec4 => Vector of 4 coordinates
    • mat2 => 2x2 matrix
    • mat4 => 4x4 homogeneous coordinate matrix
    • mat3 => 3x3 matrix
  • uniform variables => Shader variables declared with uniform storage qualifier. They allow passing data, such as transformation matrices, vectors or scalar values, from the C++-side to the shader program. Uniform variables can be used for passing the model matrix, camera matrix and projection matrix to the shader.
    • Declaration example: 'uniform mat4 u_model;'
  • attribute variables => Used for passing vertices or color coordinates from VBOs (Vertex Buffer Objects). Those variables are only accessible in the shader.
    • Declaration example: 'layout ( location = 2) in vec2 position;'
  • Output => Shader programs uses pre-defined global variables for output, for instance, gl_Position, which sets the current vertex position in clip-space (NDC - Normalized Device Coordinates).

Documentation

Source Code

Summary:

  • This sample application, draws many 4 triangles from a 2 VBO (Vertex Buffer Object) and a single VAO (Vertex Array Object). The application uses a vertex shader for performing coordinate transformation for every triangle and drawing them at different position with different rotation angles around Z axis. The triangle 'triangleD' follows the mouse position while the triangle 'triangleB' rotates acoording to the mouse position.
  • The vertex shader has two attributes variables ('position' and 'color') and two uniform variables u_model, that contains the model matrix from every rendered object, and u_projection, which contains the projection matrix.

Screenshot:

opengl-draw2d-shader.png

Vertex Shader - Source Code (Extraced from draw2d-shader.cpp)

#version 330 core

// Supplied by GPU - attribute location (id = 2)
// contains a (X, Y) vertex
layout ( location = 2)  in vec2 position;
layout ( location = 1 ) in vec3 color;     // Contains (Red, Green, Blue) components

// Output to fragment shader
out vec3 out_color;

// Model matrix => projection coordinates from the local space (model space)
// to view space .
// Transform 4x4 matrix - supplied by the C++ side.
uniform mat4 u_model;

// Projection matrix => projections camera-space coordinates
// to clip-space coordinates. (-1.0 to 1.0) range.
uniform mat4 u_projection;

void main()
{
    // gl_Position => Global output variable that holds the
    // the current vertex position. It is a vector of
    // components (X, Y, Z = 0, W = ignored)
    gl_Position = u_projection * u_model * vec4(position, 0, 1.0);

    // Forward to fragment shader
    out_color = color;
}

Fragment Shader - Source Code (Extracted from draw2d-shader.cpp)

#version 330

// This color comes from Vertex shader
in vec3 out_color;

void main()
{
    // Set vertex colors
    gl_FragColor = vec4(out_color, 1.0);

}

File: CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(GLFW_project)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)

# Set GLFW Options before FectchContent_MakeAvailable
set( GLFW_BUILD_EXAMPLES OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_TESTS    OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_DOCS     OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_INSTALL  OFF CACHE BOOL  "GLFW lib only" )

# Donwload GLFW library
FetchContent_Declare(
  glfwlib
  URL   https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
)
FetchContent_MakeAvailable(glfwlib)

# Download GLM (OpenGL math library for matrix and vectors transformation)
FetchContent_Declare(
  glm
  URL  https://github.com/g-truc/glm/archive/0.9.8.zip
)
FetchContent_MakeAvailable(glm)

message( [DEBUG] " glm_SOURCE_DIR = ${glm_SOURCE_DIR} ")
include_directories(${glm_SOURCE_DIR})

  # ======= TARGETS ===========================#

       add_executable( draw2d-shader  draw2d-shader.cpp   )
target_link_libraries( draw2d-shader  glfw OpenGL::GL GLU )

File: draw2d-shader.cpp

// Draw many colored triangles from a single VBO (Vertex Buffer Object)
#include <iostream>
#include <vector>
#include <array>
#include <cmath>
#include <functional>

// -------- OpenGL headers ---------//
//
#define GL_GLEXT_PROTOTYPES 1 // Necessary for OpenGL >= 3.0 functions
#define GL3_PROTOTYPES      1 // Necessary for OpenGL >= 3.0 functions

#include <GL/gl.h>
#include <GLFW/glfw3.h>

// #include <GL/glew.h>
#include <GL/glu.h>

// --------- OpenGL Math Librar ------------//
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/string_cast.hpp>


// Compile some  shader
void compile_shader(GLuint m_program, const char* code, GLenum type);

// Send data from memory to GPU VBO memory
void send_buffer(   GLuint* pVao        // Pointer to VAO (Vertex Array Object) - allocated by caller
                  , GLuint* pVbo        // Pointer to VBO (Vertex Buffer Object) - allocated by caller
                  , GLsizei sizeBuffer  // Total buffer size in bytes
                  , void*   pBufffer    // Pointer to buffer
                  , GLint   shader_attr // Shader attribute location id
                  , GLint   size        // Number of coordinates of a given vertex
                  , GLenum  type        // Type of each element coordinate
                  );

// ------------ Basic Data Structures -----------//

// Wrapper for 2D vertex coordinates
struct Vertex2D{  GLfloat x, y;  };
// Wrapper for RGB colors
struct ColorRGB { GLfloat r, g, b; };

constexpr ColorRGB COLOR_R = { 1.0, 0.0, 0.0  }; // Red
constexpr ColorRGB COLOR_G = { 0.0, 1.0, 0.0  }; // Green
constexpr ColorRGB COLOR_B = { 0.0, 0.0, 1.0  }; // Blue


class Model_Triangle
{
    // Object position in World Coordinates
    float _x = 0.0, _y = 0.0;
    // Scale
    float _scale = 1.0;
    // Rotation angle around z axis
    float _angle = 0.0;

public:
    Model_Triangle(float scale = 1.0){   }
    void rotate(float angle)          { _angle = angle; }
    void translate(float x , float y) { _x = x; _y = y; }
    void scale(float scale)           { _scale = scale; }
    void zoom(float factor)           {  _scale = factor + _scale; }

    // Render/draw this object
    void render(GLuint vao, GLint  u_model)
    {

        // Reset model matrix to indentiy matrix
        glm::mat4 _model(1.0);
        // Scale object (increase or decrease object size)
        _model =  glm::scale( _model, glm::vec3(_scale, _scale, _scale) );
        // Move to (X, Y) position
        _model = glm::translate( _model, glm::vec3(_x, _y, 0.0)  );
        // Rotate from a given angle around Z axis at current object X, Y  postion
        _model = glm::rotate( _model, glm::radians(_angle), glm::vec3(0, 0, 1) );

        // Set shader uniform variable.
        glUniformMatrix4fv(
            // Shader uniform variable location
            u_model
            // Number of matrices that will be set
            , 1
            // GL_FALSE => Matrix is in column-major order (Fortran matrix layout)
            // GL_TRUE  => Matrix is in row-major order (C, C++ array, matrix layout)
            , GL_FALSE
            // Pointer to first element transform matrix (homogeneous coordinate)
            , glm::value_ptr(_model)
        );
        glBindVertexArray(vao);
        // Draw 3 vertices
        glDrawArrays(GL_TRIANGLES, 0, 3);
        // Disable global state
        glBindVertexArray(0);
    }
};


// Global variable [WARNING] - Vulnerable to global "initialization fiasco"
// undefined behavior.
auto Cursor_Callback = std::function<void (GLFWwindow* window, double xpos, double ypos)>();

int main(int argc, char** argv)
{

    /* Initialize the library */
    if (!glfwInit()){ return -1; }

    // OpenGL Core Profile
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    glfwSetErrorCallback([](int error, const char* description)
                         {
                            std::cerr << " [GLFW ERROR] " << error
                                      << ": " << description
                                      << std::endl;
                         });

    GLFWwindow* window = glfwCreateWindow(640, 480, "2D Drawing with Shader", NULL, NULL);
    if (!window){ glfwTerminate(); return -1; }

    // OpenGL context
    glfwMakeContextCurrent(window);
    glClearColor(0.0f, 0.5f, 0.6f, 1.0f);

    // Mouse move callback
    // Note: It is only possible to pass non-capturing lambda to this callback.
    glfwSetCursorPosCallback( window
                            , [](GLFWwindow* window, double xpos, double ypos)
                              {
                                  // Forward call to global variable std::function<>
                                  // (Function object)
                                  Cursor_Callback(window, xpos, ypos);
                              });

    // ====== S H A D E R - C O M P I L A T I O N ====//
    //                                                //

    // Note: The shader source code is at the end of file.
    extern const char* code_vertex_shader;
    extern const char* code_frag_shader;

    GLuint prog = glCreateProgram();
    // Compile shader code
    compile_shader(prog, code_vertex_shader, GL_VERTEX_SHADER  ) ;
    compile_shader(prog, code_frag_shader,   GL_FRAGMENT_SHADER );

    // Bind current shader (enable it). Only one shader of the same type can
    // be bound at a time. For instance, only a single vertex shader can
    // be enabled at a time
    glUseProgram(prog);

    // Get shader uniform variable location for projection matrix
    // See shader code: "uniform mat4 projection;"
    const GLint u_proj  = glGetUniformLocation(prog, "u_projection");
    assert( u_proj >= 0 && "Failed to find uniform variable" );

    // Get shader uniform variable  location for model matrix.
    const GLint u_model  = glGetUniformLocation(prog, "u_model");
    assert( u_model >= 0 && "Failed to find uniform variable" );

    // Get shader attribute location - the function glGetAttribLocation - returns (-1) on error.
    const GLint attr_position = glGetAttribLocation(prog, "position");
    assert( attr_position >= 0 && "Failed to get attribute location" );
    std::fprintf(stderr, " [TRACE] Attribute location attr_position = %d \n"
                , attr_position);

    // Get shader index of color attribute variable.
    const GLint attr_color = glGetAttribLocation(prog, "color");
    assert( attr_color >= 0);


    // ====== U P L O A D - TO - G P U =========================//
    //                                                          //

    constexpr size_t NUM_VERTICES = 3;

    // Array of triangle vertex coordinates (X, Y)
    Vertex2D vertices[NUM_VERTICES] = {
      {  0.0,   0.2} // Vertex 0  (x =  0.0 ; y =  0.5)
    , { -0.2,  -0.2} // Vertex 1  (x = -0.5 ; y = -0.5)
    , {  0.2,  -0.2} // Vertex 2  (x =  0.5 ; y = -0.5)
    };

    ColorRGB colors[NUM_VERTICES] = {
          COLOR_R   // Color of vertex 0
        , COLOR_B   // Color of vertex 1
        , COLOR_G   // Color of vertex 2
    };

    // Always initialize with zero (0), otherwise, the values
    // of those variables will be undefined. (Undefine Behavior)
    GLuint vao_triangle = 0;
    GLuint vbo_vertices = 0;
    GLuint vbo_colors   = 0;

    // Send vertices to GPU VBO (vbo_vertices)
    send_buffer(  &vao_triangle    // Ponter to VAO [OUTPUT]
                , &vbo_vertices    // Pointer VBO   [OUTPUT]
                , sizeof(vertices) // Size of buffer sent to GPU (VBO memory)
                , vertices         // Pointer to buffer
                , attr_position    // Shader attribute location
                , 2                // Each vertex has 2 coordinates (X, Y)
                , GL_FLOAT         // Each vertex coordinate has type GLfloat (float)
                );

    // Send colors to GPU VBO (vbo_colors)
    send_buffer(  &vao_triangle   // Pointer Vertex Array object
                , &vbo_colors     // Pointer Vertex buffer object handle (aka token)
                , sizeof(colors)  // Buffer size
                , colors          // Pointer to buffer (addrress of first buffer element)
                , attr_color      // Shader attribute location
                , 3               // Each color has 3 coordinates (R, G, B)
                , GL_FLOAT        // Each color coordiante has type GLfloat
                );

    // ============== Set Shader Uniform Variables =============//
    //                                                          //

    int width, height;
    glfwGetWindowSize(window, &width, &height);

    // Window aspect ratio
    float aspect = static_cast<float>(width) / height;

    // Identity matrix
    const auto identity = glm::mat4(1.0);

    // Projection matrix - orthogonal projection for compensating
    // the aspect ratio.
    glm::mat4 Tproj = glm::ortho(-aspect, aspect, -1.0f, 1.0f, -1.0f, 1.0f);

    // Set projection matrix uniform variable
    glUniformMatrix4fv(u_proj, 1, GL_FALSE, glm::value_ptr(Tproj) );
    // Set model matrix uniform variable
    glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(identity) );

    // Instatiate objects that will be rendered
    auto triangleA = Model_Triangle{};
    triangleA.scale(0.50);
    triangleA.rotate(122);

    auto triangleB = Model_Triangle{};
    triangleB.translate(-0.20, 0.30);
    triangleB.rotate(50);
    triangleB.scale(1.8);

    auto triangleC = Model_Triangle{};
    triangleC.translate(0.20, 0.20);
    triangleC.rotate(35);

    // Follows mouse coordinate
    auto triangleD = Model_Triangle{};
    triangleD.translate(-0.60, -0.50);
    triangleD.scale(1.50);
    // triangleD.scale(1.2);
    // triangleD.rotate(40);

    // --------- Set mouse callback ----------//

    constexpr float PI = 3.1415926536f;
    constexpr float FACTOR_RAD_TO_DEG = 180.0 / PI;

    Cursor_Callback = [&](GLFWwindow* window, double xpos, double ypos)
    {
        // Mouse normalized coordinate
        // =>> see: https://stackoverflow.com/questions/23870750/
        float xMouse = -1.0f + 2.0f * xpos / width;
        float yMouse =  1.0f - 2.0f * ypos / height;

        // Angle in degrees between X axis vector(1, 0, 0) and poit (xMouse, yMouse, 0) position.
        // Note: atan2f() returns angle in radians.
        float angle = atan2f(yMouse, xMouse) * FACTOR_RAD_TO_DEG ;
        fprintf(stderr, " [TRACE] Angle = %f \n", angle);

        // Triangle D follows mouse position
        triangleD.translate(xMouse, yMouse);
        triangleD.rotate(angle);

        triangleB.rotate(angle);

        // Show mouse position
         // std::fprintf(stderr, " [MOUSE] x = %f ; y = %f \n", xMouse, yMouse);
    };

    //  ======= R E N D E R  - L O O P ============//
    //                                             //
    while ( !glfwWindowShouldClose(window) )
    {
        glClear(GL_COLOR_BUFFER_BIT);

        // ====== BEGIN RENDERING ============//

            triangleA.render(vao_triangle, u_model);
            triangleB.render(vao_triangle, u_model);
            triangleC.render(vao_triangle, u_model);
            triangleD.render(vao_triangle, u_model);

            //std::fprintf(stderr, " [TRACE] Redraw screen \n");

        // ====== END RENDERING ==============//

        /* Swap front and back buffers */
        glfwSwapBuffers(window);
        /* Poll for and process events */
        glfwPollEvents();

        if( glfwGetKey(window, 'Q' ) == GLFW_PRESS )
        {
             std::cout << " [TRACE] User typed Q =>> Shutdown program. Ok. " << '\n';
             break;
        }

        // Increase scale by 10%
        if( glfwGetKey(window, 'A' ) == GLFW_PRESS ){ triangleD.zoom(+0.10); }
        // Decrease scale by 10%
        if( glfwGetKey(window, 'B' ) == GLFW_PRESS ){ triangleD.zoom(-0.10); }


    }

    glfwTerminate();
    return 0;

} // --- End of main() -----//


// Minimal vertex shader =>> Runs on the GPU and processes each vertex.
const char* code_vertex_shader = R"(
    #version 330 core

    // Supplied by GPU - attribute location (id = 2)
    // contains a (X, Y) vertex
    layout ( location = 2)  in vec2 position;
    layout ( location = 1 ) in vec3 color;     // Contains (Red, Green, Blue) components

    // Output to fragment shader
    out vec3 out_color;

    // Model matrix => projection coordinates from the local space (model space)
    // to view space .
    // Transform 4x4 matrix - supplied by the C++ side.
    uniform mat4 u_model;

    // Projection matrix => projections camera-space coordinates
    // to clip-space coordinates. (-1.0 to 1.0) range.
    uniform mat4 u_projection;

    void main()
    {
        // gl_Position => Global output variable that holds the
        // the current vertex position. It is a vector of
        // components (X, Y, Z = 0, W = ignored)
        gl_Position = u_projection * u_model * vec4(position, 0, 1.0);

        // Forward to fragment shader
        out_color = color;
    }

)";

// Fragment shader source code
const char* code_frag_shader = R"(
    #version 330

    // This color comes from Vertex shader
    in vec3 out_color;

    void main()
    {
        // Set vertex colors
        gl_FragColor = vec4(out_color, 1.0);

    }
)";

    // ====== I M P L E M E N T A T I O N S ==========//

void compile_shader(GLuint m_program, const char* code, GLenum type)
{
    GLint shader_id = glCreateShader( type );
    glShaderSource(shader_id, 1, &code, nullptr);
    glCompileShader(shader_id);

    GLint is_compiled = GL_FALSE;
    // Check shader compilation result
    glGetShaderiv(shader_id, GL_COMPILE_STATUS, &is_compiled);

    // If there is any shader compilation result,
    // print the error message.
    if( is_compiled == GL_FALSE)
    {
        GLint length;
        glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &length);
        assert( length > 0 );

        std::string out(length + 1, 0x00);
        GLint chars_written;
        glGetShaderInfoLog(shader_id, length, &chars_written, out.data());
        std::cerr << " [SHADER ERROR] = " << out << '\n';
        // Abort the exection of current process.
        std::abort();
    }

    glAttachShader(m_program, shader_id);
    glDeleteShader(shader_id);
    glLinkProgram(m_program);
    GLint link_status = GL_FALSE;
    glGetProgramiv(m_program, GL_LINK_STATUS, &link_status);
    assert( link_status != GL_FALSE );
    // glUseProgram(m_program);
}

// Upload buffer from main memory to GPU VBO
// =>> Parameters VAO, VBO are allocated by the caller.
void send_buffer( GLuint* pVao, GLuint* pVbo, GLsizei sizeBuffer
                , void* pBufffer, GLint   shader_attr, GLint size
                , GLenum type)
{
    assert(pVao != nullptr);
    assert(pVbo != nullptr);
    GLuint& vao = *pVao;
    GLuint& vbo = *pVbo;

    // Generate and bind current VAO (Vertex Array Object)
    if(vao == 0){ glGenVertexArrays(1, &vao); }
    glBindVertexArray(vao);

    // Generate and bind current VBO (Vertex Buffer Object)
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);

    // Upload data to current VBO buffer in GPU
    glBufferData(GL_ARRAY_BUFFER, sizeBuffer, pBufffer, GL_STATIC_DRAW);

    glEnableVertexAttribArray(shader_attr);

    // Set data layout - how data will be interpreted.
    glVertexAttribPointer(shader_attr, size, type, GL_FALSE, 0, nullptr);

    // ------ Disable Global state set by this function -----//
    //
    // Unbind VAO
    glBindVertexArray(0);
    // Unbind VBO
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Disable current shader attribute
    glDisableVertexAttribArray(shader_attr);

}

1.20 2D - Chart with orthographic projection

This sample applications draws a 2D chart by plotting two curves, the functions f(x) and g(x). It is possible to adjust the range displayed by the chart by setting xmin, xmax, ymin and ymax using the console. By typing 'A' the chart is displayed adjusting the xmax or ymax for preserving the aspect ratio. By typing 'B' the chart does not preserve the aspect ratio and shows the full range xmin to xmax on the X axis, and ymin to ymax on the Y axis.

  • f(x) = 100.0 - x^2
  • g(x) = 50 * sin(x / (2 * PI)) + 10

The significant features on this sample code are:

  • Orthographic projection transform which allows drawing using user-defined coordinates (world-space coordinates), instead of NDC normalized-device coordinates with range -1.0 to 1.0 on each axis.
  • Sending interleaved vertex attributes, namely, positions and colors, to GPU with a single call to glBufferData() and a single VBO - Vertex Buffer Object.
  • Usage of threads for allowing the user controlling the displayed range by typing in the console (command line).

Projection matrix

The orthogonal projection matrix is used for allowing user-defined coordinates instead of normalized coordinates in the range -1.0 to 1.0 on for each axis. The orthogonal matrix without aspect-ratio compensation is generated by using the following call to glm::ortho.

glm::mat4 proj = ortho(xmin, xmax, ymin, ymax, zNear = +1.0, zFar = -1.0);

Algorithm for avoiding chart distortion

Consider the following rations \(u_x\) and \(u_y\):

\begin{eqnarray*} u_x &=& \frac{w}{\Delta_x} = \frac{ w }{ x_{max} - x_{min} } \\ u_y &=& \frac{h}{\Delta_y} = \frac{ h }{ y_{max} - y_{min} } \\ \end{eqnarray*}

Where:

  • w - screen width in pixels.
  • h - screen height in pixels.
  • \(\Delta_x\) screen width in world-space coordinates.
  • \(\Delta_y\) screen height in world-space coordinates.
  • \(u_y\) - The ratio \(u_x\) represents how many pixels (real screen lenght) each user-defined unit on world-space coordinate systems represents across the X axis.
  • \(u_x\) - The ratio \(u_y\), is similar to \(u_x\), but it is applicable to Y axis.

For instance, if the screen has width of 500 pixels and height of 600 pixels and \(x_{min} = -50\), \(x_{max} = +50\), \(y_{min} = -200\), \(y_{max} = +200\). The ratios \(u_x\) and \(u_y\) will be:

\begin{eqnarray*} \Delta_x &=& x_{max} - x_{min} = 100 \\ \Delta_y &=& y_{max} - y_{min} = 400 \\ u_x &=& \frac{500}{100} = 5 \quad \text{pixels per X axis units} \\ u_y &=& \frac{600}{400} = 1.5 \quad \text{pixels per Y axis units} \\ \end{eqnarray*}

The result means that any curve, will appear distorted, for instance a square with side of 10, will be shown on the screen with the width of 10 * ux = 50 pixels and the height of 10 * uy = 10 * 1.5 = 15 pixels. As a result, the square will appear as rectangle. To avoid any distortion, the ratios \(u_x\) and \(u_y\) must be equal, which is expressed as:

\begin{eqnarray*} u_x &=& u_y \\ \frac{x_{max} - x_{min}}{w} &=& \frac{y_{max} - y_{min}}{h} \\ \end{eqnarray*}

If the variables w, h, \(x_{min}\) and \(y_{min}\) are fixed. \(x_{max}\) or \(y_{max}\) must be adjusted for making the ratios \(u_x\) and \(u_y\) equal and avoiding any distortion.

If the adjusting is choosen for \(x_{max}\), then its new value will be:

\begin{equation} x_{max}' = x_{min} + \frac{w}{h}( y_{max} - y_{min} ) \end{equation}

If the adjusting is choosen for \(y_{max}\), then its adjusted value is:

\begin{equation} y_{max}' = y_{min} + \frac{h}{w}( x_{max} - x_{min} ) \end{equation}

By applying the adjusting for the previous case for \(x_{max}\), its adjusted value becomes:

\begin{equation} x_{max}' = (-50) + \frac{500}{600}( 200 - (-200) ) \approx 283.333 \end{equation}

The ratios \(u_x\), \(u_y\) become:

\begin{eqnarray*} u_x' &=& \frac{ w }{ x_{max}' - x_{min} } = \frac{ 500 }{ 283.333 - (-50) } \approx 1.50 \\ u_y' &=& \frac{ h }{ y_{max} - y_{min} } = \frac{ 600 }{ 200 - (-200) } = 1.50 \\ \end{eqnarray*}

As the ratios became equal, a square of side 10 will not appear distorted as its width and lenght in pixels, became equal.

Screenshots

opengl-chart-2.png

opengl-chart-1.png

Files and shaders code

Vertex Shader:

#version 330 core

layout ( location = 0) in vec2 position;
layout ( location = 1) in vec3 color;

// Forwarded to fragment shader
out vec3 out_color;

uniform mat4 u_model;  // Model matrix
uniform mat4 u_proj;   // Projection matrix

void main()
{
    // vec4(position, 0.0, 1.0) means => vec4(x, y, z = 0.0, w =1.0)
    gl_Position = u_proj * u_model * vec4(position, 0.0, 1.0);
    // Forward to fragment shader
    out_color = color;
}

Fragment Shader:

#version 330

in vec3 out_color;

void main()
{
    // Set vertex colors
    gl_FragColor =  vec4(out_color, 1.0);
}

File: CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(Draw2D-Chart-GLFW)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)

# Set GLFW Options before FectchContent_MakeAvailable
set( GLFW_BUILD_EXAMPLES OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_TESTS    OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_DOCS     OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_INSTALL  OFF CACHE BOOL  "GLFW lib only" )

# Donwload GLFW library
FetchContent_Declare(
  glfwlib
  URL   https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
)
FetchContent_MakeAvailable(glfwlib)

# Download GLM (OpenGL math library for matrix and vectors transformation)
FetchContent_Declare(
  glm
  URL  https://github.com/g-truc/glm/archive/0.9.8.zip
)
FetchContent_MakeAvailable(glm)

message( [DEBUG] " glm_SOURCE_DIR = ${glm_SOURCE_DIR} ")
include_directories(${glm_SOURCE_DIR})

  # ======= TARGETS ===========================#

       add_executable( draw2d-chart  draw2d-chart.cpp   )
target_link_libraries( draw2d-chart  glfw OpenGL::GL GLU )

File: draw2d-chart.cpp

#include <iostream>
#include <vector>
#include <array>
#include <cmath>
#include <cassert>
#include <algorithm>
#include <thread>

// -------- OpenGL headers ---------//
//
#define GL_GLEXT_PROTOTYPES
#define GL3_PROTOTYPES

#include <GL/gl.h>
#include <GLFW/glfw3.h>

// #include <GL/glew.h>
#include <GL/glu.h>
// #include <GL/glut.h>

// --------- OpenGL Math Library ------------//
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/string_cast.hpp>
#include <glm/gtc/matrix_access.hpp>

struct Point2D { GLfloat x, y;  };
struct ColorRGB{ GLfloat r, g, b; };

struct Vertex2D
{
    Point2D  position;
    ColorRGB color;
};

constexpr ColorRGB color_red    = { 1.0, 0.0, 0.0 };
constexpr ColorRGB color_green  = { 0.0, 1.0, 0.0 };
constexpr ColorRGB color_blue   = { 0.0, 0.0, 1.0 };
constexpr ColorRGB color_white  = { 1.0, 1.0, 1.0 };
constexpr ColorRGB color_yellow = { 1.0, 1.0, 0.0 };

// ------------------------------------//
// Shader code at the end of file.
extern const char*  code_vertex_shader;
extern const char*  code_frag_shader;

void        compile_shader(GLuint m_program, const char* code, GLenum type);
GLFWwindow* make_glfwWindow(int  width, int height, const char* title);

// Upload vertices position and colors to GPU in a single call.
void send_vertices( GLuint* pVao, GLuint* pVbo
                  , GLint attr_position
                  , GLint attr_color
                  , std::vector<Vertex2D> const& vertices );

// using resize_callback_t = void (*) (GLFWwindow* window, int width, int height);
using FuncResizeCallback = std::function<void (GLFWwindow* window, int width, int height)>;
FuncResizeCallback resize_callback;


/*  Ajust orthogonal projection matrix for keeping the aspect ratio.
 *  If the flag is false, the range (Xmin, Xmax), (Ymax, Ymin) is not adjusted.
 *******************************************************************/
void adjust_window_range( GLFWwindow* window
                        , GLint uniform_projection_id
                        , bool flag
                        , float xmin, float xmax, float ymin, float ymax);


template<typename Function>
auto make_curve(  ColorRGB color, float xmin, float xmax
                , size_t npoints, Function&& fun) -> std::vector<Vertex2D>
{
    std::vector<Vertex2D> curve;
    // Reserve pre-allocated space in order to avoid multiple allocations
    // via vector.push_back(Item)
    curve.reserve(npoints);
    float x = xmin;
    float y = 0.0;
    float dx = (xmax - xmin) / npoints;

    for(size_t n = 0; n < npoints; n++)
    {
        y = fun(x);
        curve.push_back( Vertex2D{ Point2D{x, y}, color } );
        // std::fprintf(stderr, " [TRACE] x = %f ; y = %f \n", x, y);
        x = x + dx;
    }
    return curve;
}

int main()
{
    // Initialize GLFW
    if (!glfwInit()){ return -1; }
    const char* title = "2D Scientific Chart with Orthogonal projection.";
    GLFWwindow* window = make_glfwWindow(600, 400, title);

    // ========== Shader settings ==================//
    //
    GLuint prog = glCreateProgram();
    // Compile shader code
    compile_shader(prog, code_vertex_shader, GL_VERTEX_SHADER  ) ;
    compile_shader(prog, code_frag_shader,   GL_FRAGMENT_SHADER );
    glUseProgram(prog);
    // ------- Shader attribute locations -------//
    //
    const GLint attr_position = glGetAttribLocation(prog, "position");
    const GLint attr_color    = glGetAttribLocation(prog, "color");
    assert( attr_color >= 0 );
    // --------- Shader Uniform Variables -------//
    //
    const GLint u_proj  = glGetUniformLocation(prog, "u_proj");
    const GLint u_model = glGetUniformLocation(prog, "u_model");
    // Note: The error checking for other uniforms are missing
    // for breviety purposes.
    assert( u_model >= 0 );

    // ------ Default values for uniform variables ----------//
    //
    const auto matrix_identity = glm::mat4(1.0);
    glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(matrix_identity));
    glUniformMatrix4fv(u_proj,  1, GL_FALSE, glm::value_ptr(matrix_identity));

    // Forward callback to global variable.
    glfwSetFramebufferSizeCallback(window, [](GLFWwindow* window, int width, int height){
         resize_callback(window, width, height);
    });

     glm::mat4 model_rectangle = glm::mat4(1.0);
    model_rectangle = glm::translate(model_rectangle, glm::vec3(5.0, 4.0, 0.0));

    // ==== O R T H O G O N A L - P R O J E C T I O N =====//
    //
    float xmin = -20.0f, xmax = +10.0f;
    float ymin = -5.0f, ymax  = +100.0f;
    float zNear = +1.0, zFar = -1.0;

    bool aspect_ratio_flag = true;
    adjust_window_range(window, u_proj, aspect_ratio_flag, xmin, xmax, ymin, ymax);

    resize_callback = [&](GLFWwindow* window, int width, int height)
    {
        // std::fprintf(stderr, " [TRACE] Window resize to width = %d ; height = %d \n", width, height);
        // Resize windo view port to the whole window
        glViewport(0, 0, width, height);
    };

    // --- Console loop thread callback =>> Allows user to adjust chart range
    // using console.
    auto console_thread = std::thread( [&]{
        // glfwMakeContextCurrent(window);
        for(;;){
            std::cout << " Enter chart range (xmin, xmax, ymin, ymax): ";
            std::cin >> xmin >> xmax >> ymin >> ymax;
            // Send an empty envent to the render loop for redrawing the window.
            // It makes the subroutine glfwWaitEvents() return to calling code.
            glfwPostEmptyEvent();

        }
    });

    console_thread.detach();

    // ======= U P L O A D - T O - G P U ===========//
    //

#if 1
    // X, Y axis lines at point (0, 0)
    auto chart_axis = std::vector<Vertex2D> {
            // X axis Line - color green
            { { -500.0, 0}, color_green }, { {500.0, 0}, color_green }
            // Y axis Line  - color blue
          , { {0, -500.0}, color_blue },  { {0, 500.0}, color_blue }
    };
    GLuint vao_axis = 0, vbo_axis = 0;
    send_vertices(&vao_axis, &vbo_axis, attr_position, attr_color, chart_axis);
#endif

    auto curve = std::vector<Vertex2D>{};

    GLuint vao_quadratic_curve = 0;
    GLuint vbo_quadratic_curve = 0;
    curve = make_curve( color_red, -20.0, 20.0, 500
                      , [](float x){ return 100.0 - x * x;  });
    GLint quadratic_curve_points = curve.size();
    send_vertices( &vao_quadratic_curve, &vbo_quadratic_curve
                 , attr_position, attr_color, curve );

    // Sine curve f(x) = 25 * sin(x / (2·PI) ) + + 60.0;
    GLuint vao_sine_curve = 0;
    GLuint vbo_sine_curve = 0;
    constexpr float PI_2 = 2.0 * 3.141592653589;
    curve = make_curve(  color_yellow, -50.0, 200.0, 500
                     , [](float x){ return 25.0 * sin(x / PI_2) + 60.0;  } );
    GLint sine_curve_points = curve.size();
    send_vertices( &vao_sine_curve, &vbo_sine_curve
                 , attr_position, attr_color, curve );


    //  ======= R E N D E R  - L O O P ============//
    //                                             //
    while ( !glfwWindowShouldClose(window) )
    {
        glClear(GL_COLOR_BUFFER_BIT  | GL_DEPTH_BUFFER_BIT);

        // ====== BEGIN RENDERING ============//

            // Adjust orthogonal projection and range of coordinates shown in the window.
            adjust_window_range(window, u_proj, aspect_ratio_flag, xmin, xmax, ymin, ymax);

            // Draw lines for X and Y axis
            //-------------------------------//
            glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(matrix_identity));
            glBindVertexArray(vao_axis);
            glDrawArrays(GL_LINES, 0, 4);

            // Draw curve quadratic curve f(x) = 100.0 - x^2
            //-------------------------------//
            glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(matrix_identity));
            glBindVertexArray(vao_quadratic_curve);
            glDrawArrays(GL_POINTS, 0, quadratic_curve_points);

            // Draw sine curve f(x) = 50 * sin(x) + 10.0;
            //-------------------------------//
            glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(matrix_identity));
            glBindVertexArray(vao_sine_curve);
            glDrawArrays(GL_LINE_STRIP, 0, sine_curve_points);


         // ====== END RENDERING ==============//

        // Swap front and back buffers
        glfwSwapBuffers(window);
        // Wait for events - blocks current thread until some event arrives.
        glfwWaitEvents();

        // ====== PROCESS EVENTS =============//

          // std::fprintf(stderr, " [TRACE] Waiting events. \n");

        if( glfwGetKey(window, 'A') == GLFW_PRESS ){  aspect_ratio_flag = true;  }
        if( glfwGetKey(window, 'B') == GLFW_PRESS ){  aspect_ratio_flag = false; }

    } // --- End of render loop --- //

    return 0;
}

// --------- S H A D E R S --------------------------------//

// Minimal vertex shader =>> Runs on the GPU and processes each vertex.
const char* code_vertex_shader = R"(
    #version 330 core

    layout ( location = 0) in vec2 position;
    layout ( location = 1) in vec3 color;

    // Forwarded to fragment shader
    out vec3 out_color;

    uniform mat4 u_model;  // Model matrix
    uniform mat4 u_proj;   // Projection matrix

    void main()
    {
        // vec4(position, 0.0, 1.0) means => vec4(x, y, z = 0.0, w =1.0)
        gl_Position = u_proj * u_model * vec4(position, 0.0, 1.0);
        // Forward to fragment shader
        out_color = color;
    }

)";

// Fragment shader source code
const char* code_frag_shader = R"(
    #version 330

    in vec3 out_color;

    void main()
    {
        // Set vertex colors
        gl_FragColor =  vec4(out_color, 1.0);
    }
)";


// --------- I M P L E M E N T A T I O N S -----------------//

GLFWwindow*
make_glfwWindow(int  width, int height, const char* title)
{

    glfwSetErrorCallback([](int error, const char* description)
                         { std::fprintf( stderr, " [GLFW ERROR] Error = %d ; Description = %s \n"
                                        , error, description);
                         });

    GLFWwindow* window = glfwCreateWindow(width, height, title, NULL, NULL);
    assert( window != nullptr && "Failed  to create Window");

    // OpenGL context
    glfwMakeContextCurrent(window);
    // Pain whole screen as black - dark screen colors are better
    // for avoding eye strain due long hours staring on monitor.
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    // Set - OpenGL Core Profile - version 3.3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // Make the window the topmost (Always on top)
    glfwWindowHint(GLFW_FLOATING, GLFW_TRUE);

    glEnable(GL_COLOR_MATERIAL);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);

    return window;
}


void compile_shader(GLuint m_program, const char* code, GLenum type)
{
    GLint shader_id = glCreateShader( type );
    glShaderSource(shader_id, 1, &code, nullptr);
    glCompileShader(shader_id);

    GLint is_compiled = GL_FALSE;
    // Check shader compilation result
    glGetShaderiv(shader_id, GL_COMPILE_STATUS, &is_compiled);

    // If there is any shader compilation result,
    // print the error message.
    if( is_compiled == GL_FALSE)
    {
        std::cerr << " [SHADER ERROR] =>> Abort execution. " << '\n';
        // Abort the exection of current process.
        std::abort();
    }

    glAttachShader(m_program, shader_id);
    glDeleteShader(shader_id);
    glLinkProgram(m_program);
    GLint link_status = GL_FALSE;
    glGetProgramiv(m_program, GL_LINK_STATUS, &link_status);
    assert( link_status != GL_FALSE );
    // glUseProgram(m_program);
}


// Upload vertices from main memory to GPU memory.
// Vertices position and colors are uploaded in a single call.
void send_vertices( GLuint* pVao, GLuint* pVbo
                  , GLint attr_position
                  , GLint attr_color
                  , std::vector<Vertex2D> const& vertices )
{
    assert(pVao != nullptr);
    assert(pVbo != nullptr);
    GLuint& vao = *pVao;
    GLuint& vbo = *pVbo;
    // Generate and bind current VAO (Vertex Array Object)
    if(vao == 0){ glGenVertexArrays(1, &vao); }
    glBindVertexArray(vao);
    // Generate and bind current VBO (Vertex Buffer Object)
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Upload data to current VBO buffer in GPU
    glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex2D)
                , vertices.data(), GL_STATIC_DRAW);

    glEnableVertexAttribArray(attr_position);
    glEnableVertexAttribArray(attr_color);

    static_assert( sizeof(Vertex2D) == 5 * sizeof(float)
                 ,"Vertex2D size assumption does not hold." );

    static_assert( sizeof(Point2D) == offsetof(Vertex2D, color)
                 , "Invalid offsetof() assumption");

    // Set data layout - how data will be interpreted.
    // => Each vertex has 2 coordinates.
    glVertexAttribPointer( attr_position     // Position vertex shader attribute
                          , 2                // Each position has 2 coordinates (X, Y)
                          , GL_FLOAT         // Type of each coordinate
                          , GL_FALSE
                          , sizeof(Vertex2D) // Offset to next coordinates (5 floats in this case)
                          , nullptr
                          );

    glVertexAttribPointer( attr_color
                           // Each color has 3 components R, G, B
                          , 3
                           // Type of each color coordinate
                          , GL_FLOAT
                          , GL_FALSE
                           // Offset to next coordinates (next row)
                          , sizeof(Vertex2D)
                           // Offset to color member variable in class Vertex2D
                          , reinterpret_cast<void*>( offsetof(Vertex2D, color) )
                          );

    // ------ Disable Global state set by this function -----//
    // Unbind VAO
    glBindVertexArray(0);
    // Unbind VBO
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Disable current shader attribute
    glDisableVertexAttribArray(attr_position);
    glDisableVertexAttribArray(attr_color);
}

void adjust_window_range( GLFWwindow* window
                        , GLint uniform_projection_id
                        , bool flag
                        , float xmin, float xmax, float ymin, float ymax)
{
    constexpr float zFar  = -1.0f;
    constexpr float zNear = +1.0f;

    if(flag == false)
    {
        glm::mat4 proj_1 = glm::ortho(xmin, xmax, ymin, ymax, zNear, zFar);
        glUniformMatrix4fv(uniform_projection_id, 1, GL_FALSE, glm::value_ptr(proj_1));
        return;
    }

    int width, height;
    glfwGetWindowSize(window, &width, &height);
    float k_aspect = static_cast<float>(width) / static_cast<float>(height);
    float dx = xmax - xmin;
    float dy = ymax - ymin;

    if(dx > dy){
        ymax =  ymin + (1.0f / k_aspect ) * dx;
        // std::fprintf(stderr, " [TRACE] Adjust range => ymax = %f \n", ymax);
    } else {
        xmax = xmin + k_aspect * dy;
        // std::fprintf(stderr, " [TRACE] Adjust range => xmax = %f \n", ymax);
    }

    dx = xmax - xmin;
    dy = ymax - ymin;
    float ux = width / dx;
    float uy = height / dy;

    // std::fprintf(stderr, " [TRACE] ux = %f ; uy = %f \n", ux, uy);

    glm::mat4 proj_2 = glm::ortho(xmin, xmax, ymin, ymax, zNear, zFar);
    glUniformMatrix4fv(uniform_projection_id, 1, GL_FALSE, glm::value_ptr(proj_2));
}

1.21 2D - IBO - Index Buffer Object

This code uses OpenGL IBO (Index Buffer Objects) for drawing triangles and lines OpenGL primitives. The IBO is useful for drawing complex objects without repeating vertices, which saves memory and bandwith and increases performance.

Screenshot

opengl-ibo-triangles.png

Figure 15: Triangles IBO - Index Buffer Object

Code Highlights

Vertices and colors data structures:

struct Point2D  { GLfloat x, y;    };
struct ColorRGB { GLfloat r, g, b; };
struct Vertex2D { Point2D  position; ColorRGB color; };

using VertexArray = std::vector<Vertex2D>;
using IndexArray  = std::vector<GLuint>;

The next blocks defines the triangles vertices and the connection between those vertices. Each three indices in the 'triangles_indices' variables represents a triangle. Without using IBO (Index Buffer Object), there would be duplicated vertices, then the content of array 'vertices' would be [V0, V1, V2, V1, V3, V4, V3, V5, V6, V2, V7, V5],

// Triangle side 
float a = 1.0 / 5.0; 
// Triangle height 
float h = sqrt(3.0) * a;

auto vertices = VertexArray {
      Vertex2D{ { 0,       0  }, color_green   } // V0 - vertex 0 
    , Vertex2D{ { 2 * a,   0  }, color_green   } // V1 
    , Vertex2D{ { a,       h  }, color_green   } // V2 

    , Vertex2D{ { 3 * a,   h  }, color_blue    } // V3
    , Vertex2D{ { 4 * a,   0  }, color_blue    } // V4

    , Vertex2D{ { 2 * a, 2 * h }, color_red    } // V5
    , Vertex2D{ { 4 * a, 2 * h }, color_red    } // V6
    , Vertex2D{ {     0, 2 * h }, color_yellow } // V7 - vertex 7 
};

// Indices for GL_TRIANGLE drawing primitive. 
auto triangles_indices = IndexArray {
      0, 1, 2  // Draw triangle with vertices V0, V1, V2 (Triangle A)
    , 1, 3, 4  // Draw triangle with vertices V1, V3, V4 (Triangle B)
    , 3, 5, 6  // Draw triangle with vertices V3, V5, V6 (Triangle C)
    , 2, 7, 5  // Draw triangle with vertices V2, V7, V5 (Triangle D)
};

This index array defines the connections between vertices for GL_LINES primitives, each 2 indices represents a line.

// Indices for GL_LINES drawing primitive 
 auto lines_indices = IndexArray {
       0, 1 // Line connecting vertices V0 and V1 
     , 0, 2 // Line connecting vertices V0 and V2 
     , 1, 2 // Line connecting vertices V1 and V2 

     , 2, 7 
     , 5, 7 
     , 2, 5 

     , 1, 3
     , 1, 4
     , 3, 4

     , 3, 5
     , 3, 6
     , 5, 6
 };

In the following code highlight, the vertices indices and data are sent to the GPU using the subroutines send_vertices() and send_indices(). These functions uses the following parameters: vao (Vertex Array Object); vbo (Vertex Buffer Object) and ibo (Index Buffer Objects), which are supposed to be allocated by calling code.

// ------- Upload data for drawing traingle primitives --------//
//
GLuint vao_triangles = 0, vbo_triangles = 0, ibo_triangles = 0;    

send_vertices( &vao_triangles, &vbo_triangles
             , attr_position, attr_color, vertices);

send_indices(&vao_triangles, &ibo_triangles, triangles_indices);


// -------- Upload data for drawing lines primitives ----------// 
//
GLuint vao_lines = 0, vbo_lines = 0, ibo_lines = 0;  
send_vertices( &vao_lines, &vbo_lines
             , attr_position, attr_color, vertices);     
send_indices(&vao_lines, &ibo_lines, lines_indices);

At the rendering loop. The OpenGL glDrawElements() subroutine is used ,instead of glDrawArrays(), for drawing triangles and lines primitives. This subroutine uses the vertices indices instead of using the vertices ordering. For instance, if glDrawArrays() was used, each three vertices from the current bound VBO would be used for drawing a triangle.

// ====== BEGIN RENDERING ============//

    // ---- Draw triangles primitives using IBO -------// 
    glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(model_triangle_primtives));
    glBindVertexArray(vao_triangles);
    // Draw OpenGL primitives between vertices designated by their
    // IBO - Index Buffer Object.         
    glDrawElements(GL_TRIANGLES, triangles_indices.size() , GL_UNSIGNED_INT, nullptr);

    // ----- Draw lines using IBO -----------------------//
    glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(model_lines_primitives));
    glBindVertexArray(vao_lines);
    glDrawElements(GL_LINES, lines_indices.size() , GL_UNSIGNED_INT, nullptr);            

// ====== END RENDERING ==============//

Files / Sources

File: CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(Draw2D-Chart-GLFW)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)

# Set GLFW Options before FectchContent_MakeAvailable
set( GLFW_BUILD_EXAMPLES OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_TESTS    OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_DOCS     OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_INSTALL  OFF CACHE BOOL  "GLFW lib only" )

# Donwload GLFW library
FetchContent_Declare(
  glfwlib
  URL   https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
)
FetchContent_MakeAvailable(glfwlib)

# Download GLM (OpenGL math library for matrix and vectors transformation)
FetchContent_Declare(
  glm
  URL  https://github.com/g-truc/glm/archive/0.9.8.zip
)
FetchContent_MakeAvailable(glm)

message( [DEBUG] " glm_SOURCE_DIR = ${glm_SOURCE_DIR} ")
include_directories(${glm_SOURCE_DIR})

  # ======= TARGETS ===========================#

       add_executable( draw2d-ibo-triangles  draw2d-ibo-triangles.cpp   )
target_link_libraries( draw2d-ibo-triangles  glfw OpenGL::GL GLU )

File: draw2d-ibo-triangles.cpp

#include <iostream>
#include <vector> 
#include <array>
#include <cmath>
#include <cassert>

// -------- OpenGL headers ---------//
//
#define GL_GLEXT_PROTOTYPES 1 // Necessary for OpenGL >= 3.0 functions
#define GL3_PROTOTYPES      1 // Necessary for OpenGL >= 3.0 functions

#include <GL/gl.h>
#include <GLFW/glfw3.h>

#include <GL/gl.h>
#include <GLFW/glfw3.h>

// #include <GL/glew.h>
#include <GL/glu.h>

// --------- OpenGL Math Library ------------//
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/string_cast.hpp>
#include <glm/gtc/matrix_access.hpp>

struct Point2D  { GLfloat x, y;    };
struct ColorRGB { GLfloat r, g, b; };
struct Vertex2D { Point2D  position; ColorRGB color; };

extern const char*  code_vertex_shader;
extern const char*  code_frag_shader;

void        compile_shader(GLuint m_program, const char* code, GLenum type);
GLFWwindow* make_glfwWindow(int  width, int height, const char* title);

// Send vertices (position and color of each vertex to GPU memory)
void send_vertices( GLuint* pVao, GLuint* pVbo
                  , GLint attr_position
                  , GLint attr_color 
                  , std::vector<Vertex2D> const& vertices );

// Send vertices indices to GPU memory. 
void send_indices(GLuint* pVao, GLuint* pVbi
                , std::vector<GLuint> const& indices);

constexpr ColorRGB color_red    = { 1.0, 0.0, 0.0 };
constexpr ColorRGB color_green  = { 0.0, 1.0, 0.0 };        
constexpr ColorRGB color_blue   = { 0.0, 0.0, 1.0 };
constexpr ColorRGB color_white  = { 1.0, 1.0, 1.0 };
constexpr ColorRGB color_yellow = { 1.0, 1.0, 0.0 };

int main(int argc, char** argv)
{
    if (!glfwInit()){ return -1; }

    /* Create a windowed mode window and its OpenGL context */
    GLFWwindow* window = make_glfwWindow(640, 480, "Draw 2D - IBO - Index Buffer Object");

    // ======= S H A D E R - C O M P I L A T I O N ==========// 
    // 
    GLuint prog = glCreateProgram();
    // Compile shader code 
    compile_shader(prog, code_vertex_shader, GL_VERTEX_SHADER  ) ;    
    compile_shader(prog, code_frag_shader,   GL_FRAGMENT_SHADER );
    glUseProgram(prog);    
    // ------- Shader attribute locations -------//
    const GLint attr_position = glGetAttribLocation(prog, "position");
    const GLint attr_color    = glGetAttribLocation(prog, "color");
    assert( attr_position >= 0 );
    assert( attr_color >= 0 );
    // --------- Shader Uniform Variables -------//
    const GLint u_proj  = glGetUniformLocation(prog, "u_proj");
    const GLint u_model = glGetUniformLocation(prog, "u_model");
    assert(u_proj >= 0);
    assert(u_model >= 0);

    // ------ Default values for uniform variables ----------//
    const auto matrix_identity = glm::mat4(1.0);
    glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(matrix_identity));
    glUniformMatrix4fv(u_proj,  1, GL_FALSE, glm::value_ptr(matrix_identity));


    //  ======= U P L O A D - T O - G P U =====================//
    //     

    using VertexArray = std::vector<Vertex2D>;
    using IndexArray = std::vector<GLuint>;

    // Triangle side 
    float a = 1.0 / 5.0; 
    // Triangle height 
    float h = sqrt(3.0) * a;

    auto vertices = VertexArray {
          Vertex2D{ { 0,       0  }, color_green   } // V0 - vertex 0 
        , Vertex2D{ { 2 * a,   0  }, color_green   } // V1 
        , Vertex2D{ { a,       h  }, color_green   } // V2 

        , Vertex2D{ { 3 * a,   h  }, color_blue    } // V3
        , Vertex2D{ { 4 * a,   0  }, color_blue    } // V4

        , Vertex2D{ { 2 * a, 2 * h }, color_red    } // V5
        , Vertex2D{ { 4 * a, 2 * h }, color_red    } // V6
        , Vertex2D{ {     0, 2 * h }, color_yellow } // V7 - vertex 7 
    };

    // Indices for GL_TRIANGLE drawing primitive. 
    auto triangles_indices = IndexArray {
          0, 1, 2  // Draw triangle with vertices V0, V1, V2 (Triangle A)
        , 1, 3, 4  // Draw triangle with vertices V1, V3, V4 (Triangle B)
        , 3, 5, 6  // Draw triangle with vertices V3, V5, V6 (Triangle C)
        , 2, 7, 5  // Draw triangle with vertices V2, V7, V5 (Triangle D)
    };

    // Indices for GL_LINES drawing primitive 
    auto lines_indices = IndexArray {
          0, 1 // Line connecting vertices V0 and V1 
        , 0, 2 // Line connecting vertices V0 and V2 
        , 1, 2 // Line connecting vertices V1 and V2 

        , 2, 7 
        , 5, 7 
        , 2, 5 

        , 1, 3
        , 1, 4
        , 3, 4

        , 3, 5
        , 3, 6
        , 5, 6
    };

    // ------- Upload data for drawing traingle primitives --------//
    //
    GLuint vao_triangles = 0, vbo_triangles = 0, ibo_triangles = 0;    

    send_vertices( &vao_triangles, &vbo_triangles
                 , attr_position, attr_color, vertices);

    send_indices(&vao_triangles, &ibo_triangles, triangles_indices);


    // -------- Upload data for drawing lines primitives ----------// 
    //
    GLuint vao_lines = 0, vbo_lines = 0, ibo_lines = 0;  
    send_vertices( &vao_lines, &vbo_lines
                 , attr_position, attr_color, vertices);     
    send_indices(&vao_lines, &ibo_lines, lines_indices);

    // ==== Model matrices for translating drawings =======//
    //      
    glm::mat4 model_triangle_primtives = matrix_identity;
    glm::mat4 model_lines_primitives = matrix_identity;   
    // Translate triangles to point (x = -0.7, y = -0.8, z = 0.0)
    model_triangle_primtives = glm::translate(model_triangle_primtives, glm::vec3(-0.7, -0.8, 0.0));

    //  ======= R E N D E R  - L O O P ===================//
    //                                                    //
    while ( !glfwWindowShouldClose(window) )
    {
        glClear(GL_COLOR_BUFFER_BIT  | GL_DEPTH_BUFFER_BIT);

        // ====== BEGIN RENDERING ============//

            // ---- Draw triangles primitives using IBO -------// 
            glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(model_triangle_primtives));
            glBindVertexArray(vao_triangles);
            // Draw OpenGL primitives between vertices designated by their
            // IBO - Index Buffer Object.         
            glDrawElements(GL_TRIANGLES, triangles_indices.size() , GL_UNSIGNED_INT, nullptr);

            // ----- Draw lines using IBO -----------------------//
            glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(model_lines_primitives));
            glBindVertexArray(vao_lines);
            glDrawElements(GL_LINES, lines_indices.size() , GL_UNSIGNED_INT, nullptr);            

        // ====== END RENDERING ==============//

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

// ---------- S H A D E R S - C O D E  ---------------------// 

// Minimal vertex shader =>> Runs on the GPU and processes each vertex.
const char* code_vertex_shader = R"(    
    #version 330 core 

    layout ( location = 0) in vec2 position;
    layout ( location = 1) in vec3 color;   

    // Forwarded to fragment shader 
    out vec3 out_color;

    uniform mat4 u_model;  // Model matrix 
    uniform mat4 u_proj;   // Projection matrix 

    void main()
    {
        // vec4(position, 0.0, 1.0) means => vec4(x, y, z = 0.0, w =1.0)
        gl_Position = u_proj * u_model * vec4(position, 0.0, 1.0);

        // Forward to fragment shader 
        out_color = color; 
    }

)";

// Fragment shader source code 
const char* code_frag_shader = R"(
    #version 330 

    in vec3 out_color;

    void main()
    {
        // Set vertex colors
        gl_FragColor =  vec4(out_color, 1.0);
    }
)";

// --------- I M P L E M E N T A T I O N S -----------------//

GLFWwindow* 
make_glfwWindow(int  width, int height, const char* title)
{

    glfwSetErrorCallback([](int error, const char* description)
                         { std::fprintf( stderr, " [GLFW ERROR] Error = %d ; Description = %s \n"
                                        , error, description);
                         });

    GLFWwindow* window = glfwCreateWindow(width, height, title, NULL, NULL);
    assert( window != nullptr && "Failed  to create Window");

    // OpenGL context 
    glfwMakeContextCurrent(window);
    // Set window color 
    glClearColor(0.0f, 0.5f, 3.0f, 1.0f);
    // Set - OpenGL Core Profile - version 3.3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // Make the window the topmost (Always on top)
    glfwWindowHint(GLFW_FLOATING, GLFW_TRUE);

    glEnable(GL_COLOR_MATERIAL);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);

    return window;
}


void compile_shader(GLuint m_program, const char* code, GLenum type)
{
    GLint shader_id = glCreateShader( type );
    glShaderSource(shader_id, 1, &code, nullptr);
    glCompileShader(shader_id);

    GLint is_compiled = GL_FALSE;
    // Check shader compilation result 
    glGetShaderiv(shader_id, GL_COMPILE_STATUS, &is_compiled);

    // If there is any shader compilation result, 
    // print the error message.
    if( is_compiled == GL_FALSE)
    {
        std::cerr << " [SHADER ERROR] =>> Abort execution. " << '\n';
        // Abort the exection of current process. 
        std::abort();
    }          

    glAttachShader(m_program, shader_id);   
    glDeleteShader(shader_id);
    glLinkProgram(m_program);
    GLint link_status = GL_FALSE;
    glGetProgramiv(m_program, GL_LINK_STATUS, &link_status);
    assert( link_status != GL_FALSE );   
    // glUseProgram(m_program);         
}

// Upload vertices from main memory to GPU memory.
// Vertices position and colors are uploaded in a single call.
void send_vertices( GLuint* pVao, GLuint* pVbo
                  , GLint attr_position
                  , GLint attr_color 
                  , std::vector<Vertex2D> const& vertices )
{
    assert(pVao != nullptr);
    assert(pVbo != nullptr);
    GLuint& vao = *pVao;
    GLuint& vbo = *pVbo;
    // Generate and bind current VAO (Vertex Array Object)
    if(vao == 0){ glGenVertexArrays(1, &vao); }
    glBindVertexArray(vao);
    // Generate and bind current VBO (Vertex Buffer Object)
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Upload data to current VBO buffer in GPU 
    glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex2D)
                , vertices.data(), GL_STATIC_DRAW);   

    glEnableVertexAttribArray(attr_position);    
    glEnableVertexAttribArray(attr_color);    

    // Set data layout - how data will be interpreted.
    // => Each vertex has 2 coordinates. 
    glVertexAttribPointer( attr_position, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), nullptr);

    glVertexAttribPointer( attr_color, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex2D)     
                           // Offset to color member variable in class Vertex2D
                          , reinterpret_cast<void*>( offsetof(Vertex2D, color) )
                          );

    // ------ Disable Global state set by this function -----//
    // Unbind VAO 
    glBindVertexArray(0);
    // Unbind VBO 
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Disable current shader attribute 
    glDisableVertexAttribArray(attr_position);
    glDisableVertexAttribArray(attr_color);
}

void send_indices(GLuint* pVao, GLuint* pVbi, std::vector<GLuint> const& indices)
{
    GLuint& vao = *pVao;
    GLuint& vbi = *pVbi;
    glBindVertexArray(vao);
    // Generate index buffer object 
    glGenBuffers(1, &vbi); 
    // Bind this index buffer object - only one IBO can 
    // be bound at a time. (It is a global state!!)
    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, vbi );
    // Upload indices to GPU - This VBI object is a handle 
    // (token, akin to file descriptor) that refers 
    // to the data sent to the GPU on the next line. 
    glBufferData( GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(GLuint)
                    , indices.data(), GL_STATIC_DRAW);    
    // ----------- Unset global state -------------//
    // 
    glBindVertexArray(0);
    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );
}

1.22 3D - Rotating cube/box

This sample application contains: two cubes; two grid planes for visual reference; a coordinate axis for visual debugging and a camera that always points to the origin. The camera has the following controls: right arrow, rotates the camera's position in a counterclockwise way around Y axis (red line); if left arrow is typed, the camera position rotates in a clockwise way around Y axis (red line). If up arrow is typed, the camera position moves in the positive direction of Y axis. If down arrow is typed, the reverse happens.

Screenshots

opengl-draw3d-rotating-cube1.png

opengl-draw3d-rotating-cube2.png

Project Files

Vertex Shader Code:

#version 330 core

layout ( location = 0)  in vec3 position;
layout ( location = 1) in vec3 color;
out vec3 out_color;
uniform mat4 u_model;       // Model matrix
uniform mat4 u_view;        // Camera's view matrix
uniform mat4 u_projection;  // Camera's projection matrix

void main()
{
    gl_Position = u_projection * u_view * u_model * vec4(position, 1.0);

    // Forward to fragment shader
    out_color = color; // vec3(0.56, 0.6, 0.0);
}

Fragment Shader Code:

#version 330

// This color comes from Vertex shader
in vec3 out_color;

void main()
{
    // Set vertex colors
    gl_FragColor =  vec4(out_color, 1.0);
    // gl_FragColor = vec4(0.3, 0.6, 0.0, 1.0);
}

File: CmakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(GLFW_project)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)

# Set GLFW Options before FectchContent_MakeAvailable
set( GLFW_BUILD_EXAMPLES OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_TESTS    OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_DOCS     OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_INSTALL  OFF CACHE BOOL  "GLFW lib only" )

# Donwload GLFW library
FetchContent_Declare(
  glfwlib
  URL   https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
)
FetchContent_MakeAvailable(glfwlib)

# Download GLM (OpenGL math library for matrix and vectors transformation)
FetchContent_Declare(
  glm
  URL  https://github.com/g-truc/glm/archive/0.9.8.zip
)
FetchContent_MakeAvailable(glm)

# Download pre-compiled GLEW when building under Windows NT OS (x64)
IF(WIN32)
   FetchContent_Declare(
      glew-release 
      URL  https://ufpr.dl.sourceforge.net/project/glew/glew/2.1.0/glew-2.1.0-win32.zip
   )
   FetchContent_MakeAvailable(glew-release)
   include_directories( ${glew-release_SOURCE_DIR}/include  ${glm_SOURCE_DIR} )        
   link_directories(  ${glew-release_SOURCE_DIR}/lib/Release/x64 )

   set( GLEW_LIB_PATH1 ${glew-release_SOURCE_DIR}/lib/Release/x64/glew32.lib )
   set( GLEW_LIB_PATH2 ${glew-release_SOURCE_DIR}/lib/Release/x64/glew32s.lib )
ENDIF()


message( [DEBUG] " glm_SOURCE_DIR = ${glm_SOURCE_DIR} ")
include_directories(${glm_SOURCE_DIR})

MACRO(ADD_OPENGL_APP target sources)
   add_executable( ${target} ${sources} )
   message([TRACE] " Add OpenGL executable = ${target} ")

   target_link_libraries( ${target} glfw
                                    OpenGL::GL
                                    ${GLEW_LIB_PATH1}
                                    ${GLEW_LIB_PATH2} )

    IF(MINGW)
        # Statically link against MINGW dependencies
        # for making easier to deploy on other machines. 
        target_link_options( ${target} PRIVATE                                 
                                 -static-libgcc
                                 -static-libstdc++
                                 -Wl,-Bstatic,--whole-archive -lwinpthread
                                 -Wl,--no-whole-archive                                    
                                 )
     ENDIF()       


     # Copy GLEW DLL shared library to same directory as the executable.                 
     IF(WIN32)                           
        add_custom_command(TARGET ${target} POST_BUILD 
                       COMMAND ${CMAKE_COMMAND} -E copy_if_different
                       "${glew-release_SOURCE_DIR}/bin/Release/x64/glew32.dll"              
                       $<TARGET_FILE_DIR:${target}>)
     ENDIF()                                       
ENDMACRO()     


  # ======= TARGETS ===========================#

  ADD_OPENGL_APP( draw3d-cube draw3d-cube.cpp )

File: draw3d-cube.cpp

// Draw many colored triangles from a single VBO (Vertex Buffer Object)
#include <iostream>
#include <vector>
#include <array>
#include <cmath>
#include <functional>

// -------- OpenGL headers ---------//
//
#define GL_GLEXT_PROTOTYPES 1 // Necessary for OpenGL >= 3.0 functions
#define GL3_PROTOTYPES      1 // Necessary for OpenGL >= 3.0 functions

#if defined(_WIN32)
   #include <windows.h>
   #include <GL/glew.h>
#endif

#include <GL/gl.h>
#include <GLFW/glfw3.h>

// #include <GL/glew.h>
#include <GL/glu.h>

// --------- OpenGL Math Librar ------------//
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/string_cast.hpp>

// #define GLUT_ENABLED

#if defined(GLUT_ENABLED)
    #include <GL/glut.h>
#endif

#define GL_CHECK(funcall)\
    do { \
        (funcall); \
        GLint error = glGetError();  \
        if(error == GL_NO_ERROR){ break; } \
        std::fprintf(stderr, " [OPENGL ERROR] Error code = %d ; line = %d ; call = '%s'  \n" \
                      , error, __LINE__, #funcall ); \
        abort(); \
    } while(0)

GLFWwindow* make_glfwWindowi(int  width, int height, const char* title);

// Compile some  shader
void compile_shader(GLuint m_program, const char* code, GLenum type);

// Send data from memory to GPU VBO memory
void send_buffer(   GLuint* pVao        // Pointer to VAO (Vertex Array Object) - allocated by caller
                  , GLuint* pVbo        // Pointer to VBO (Vertex Buffer Object) - allocated by caller
                  , GLsizei sizeBuffer  // Total buffer size in bytes
                  , void*   pBufffer    // Pointer to buffer
                  , GLint   shader_attr // Shader attribute location id
                  , GLint   size        // Number of coordinates of a given vertex
                  , GLenum  type        // Type of each element coordinate
                  );

// ------------ Basic Data Structures -----------//

// Wrapper for 2D vertex coordinates
struct Vertex3D{  GLfloat x, y, z;  };
// Wrapper for RGB colors
struct ColorRGB { GLfloat r, g, b; };

struct Geometry {
    std::vector<Vertex3D> vertices;
    std::vector<ColorRGB> colors;
};

struct RenderObject {
    GLuint     u_model;     // Shader uniform variable for model matrix
    GLuint     vao;         // Vertex Array object
    glm::mat4  model;       // Object model matrix
    GLenum     draw_type;   // Draw type
    GLint      n_vertices;  // Number of vertices

    RenderObject(){ model = glm::mat4(1.0);  }
    // Explicit copy constructor
    RenderObject(const RenderObject&) = default;
    // Explicit copy assignment operator
    RenderObject& operator=(const RenderObject&) = default;

    void render()
    {
        glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(model) );
        glBindVertexArray(vao);
        glDrawArrays(draw_type, 0, n_vertices);
    }
};


Geometry make_grid_geometry(size_t n_grid, float dx, const ColorRGB& color);
Geometry make_cube_geometry(float w);

constexpr ColorRGB color_red   = {1.0, 0.0, 0.0};
constexpr ColorRGB color_green = {0.0, 1.0, 0.0};
constexpr ColorRGB color_blue  = {0.0, 0.0, 1.0};
constexpr ColorRGB color_yelllow    = { 0.80,  1.000, 0.100 };
constexpr ColorRGB color_gray       = { 0.47,  0.390, 0.380 };
constexpr ColorRGB color_dark_green = { 0.027, 0.392, 0.050 };
constexpr ColorRGB color_dark_blue  = { 0.109, 0.066, 0.411 };

// Send vertices to GPU
void send_vertices( GLuint* pVao, GLuint* pVbo,  GLint shader_attr
                ,   std::vector<Vertex3D>& vertices )
{
    send_buffer(pVao, pVbo, sizeof(Vertex3D) * vertices.size(), vertices.data(), shader_attr, 3, GL_FLOAT );
}

// Send color coordinates to GPU
void send_colors( GLuint* pVao, GLuint* pVbo,  GLint shader_attr
                ,   std::vector<ColorRGB>& vertices )
{
    send_buffer(pVao, pVbo, sizeof(Vertex3D) * vertices.size(), vertices.data(), shader_attr, 3, GL_FLOAT );
}

struct Camera{
    // Current location of camera in world coordinates
    glm::vec3 cam_eye  = { 2.0, 4.0, 5.0 };
    // Point to where the camera is looking at in world coordinates.
    glm::vec3 cam_targ = { 0.0, 0.0, 0.0  };
    // Current  camera up vector (orientation) - Y axis (default)
    glm::vec3 cam_up = { 0.0, 1.0, 0.0 };

    // Field of view
    float fov_angle = glm::radians(60.0);
    // Aspect ratio
    float aspect   = 1.0;
    float zFar     = 20.0;
    float zNear    = 0.1;

    // ID of shader's view uniform variable - for setting view matrix
    GLuint shader_uniform_view;
    // ID of shader's projection uniform variable for setting projection matrix.
    GLuint shader_uniform_proj;

    Camera(GLuint uniform_view, GLuint uniform_proj, float aspect):
        shader_uniform_view(uniform_view)
      , shader_uniform_proj(uniform_proj)
      , aspect(aspect)
    {  update_view();  }

    void update_view()
    {
        // Create View matrix that maps from world-space coordinates to
        // camera-space coordinates
        auto Tview = glm::lookAt(cam_eye, cam_targ, cam_up);

        // Create projection matrix coordinates that maps from
        // camera-space coordinates to NDC (Normalized Device Coordinates).
        auto Tproj = glm::perspective( fov_angle
                                     , aspect
                                     , zNear
                                     , zFar );
        // Set shader uniform variables.
        glUniformMatrix4fv(shader_uniform_view, 1, GL_FALSE, glm::value_ptr(Tview) );
        glUniformMatrix4fv(shader_uniform_proj, 1, GL_FALSE, glm::value_ptr(Tproj) );

    }

    // Rotate camera around Y axis
    void rotate_y(float angle)
    {
        float a = glm::radians(angle);
        float C = cosf(a), S = sinf(a);
        float x = this->cam_eye.x;
        float y = this->cam_eye.y;
        float z = this->cam_eye.z;
        // Apply Y-axis rotation matrix directly to Y axis.
        this->cam_eye.x = x * C + z * S;
        this->cam_eye.y = y;
        this->cam_eye.z = -x * S + z * C;
        this->update_view();
    }

};

int main(int argc, char** argv)
{

    /* Initialize the library */
    if (!glfwInit()){ return -1; }

    #if defined(GLUT_ENABLED)
        glutInit(&argc, argv);
    #endif

    // ====== S H A D E R - C O M P I L A T I O N ====//
    //                                                //

    GLFWwindow* window = make_glfwWindowi(640, 480, "Draw Cube 3D");

    // Note: The shader source code is at the end of file.
    extern const char* code_vertex_shader;
    extern const char* code_frag_shader;

    GLuint prog = glCreateProgram();
    // Compile shader code
    compile_shader(prog, code_vertex_shader, GL_VERTEX_SHADER  ) ;
    compile_shader(prog, code_frag_shader,   GL_FRAGMENT_SHADER );
    glUseProgram(prog);

    // Get shader uniform variable location for projection matrix
    // See shader code: "uniform mat4 projection;"
    const GLint u_proj  = glGetUniformLocation(prog, "u_projection");
    assert( u_proj >= 0 && "Failed to find u_projection uniform variable" );

    const GLint u_view = glGetUniformLocation(prog, "u_view");
    assert( u_proj >= 0 && "Failed to find u_view uniform variable" );

    // Get shader uniform variable  location for model matrix.
    const GLint u_model  = glGetUniformLocation(prog, "u_model");
    assert( u_model >= 0 && "Failed to find uniform variable" );

    // Get shader attribute location - the function glGetAttribLocation - returns (-1) on error.
    const GLint attr_position = glGetAttribLocation(prog, "position");
    assert( attr_position >= 0 && "Failed to get attribute location" );

    // Get shader attribute of color
    const GLint attr_color = glGetAttribLocation(prog, "color");
    if( attr_color < 0){ std::fprintf(stderr, " [WARNING] Shader color attribute location not found. \n"); };


    // ====== U P L O A D - TO - G P U =========================//
    //                                                          //

    // ----- Upload Cube vertices and colors ----------------//
        Geometry cube_geometry = make_cube_geometry(0.3);
    GLuint vao_cube     = 0;
    GLuint vbo_vertices = 0;
    GLuint vbo_colors   = 0;
    // Upload geometry cube data to GPU
    send_vertices(&vao_cube, &vbo_vertices, attr_position, cube_geometry.vertices);
    send_colors(&vao_cube, &vbo_colors, attr_color, cube_geometry.colors);
    std::fprintf(stderr, " [TRACE] vao_cube = %d \n", vao_cube );

    // ------- Upload grid vertices and colors ------------------//
    Geometry grid_geometry = make_grid_geometry(20, 0.1f, ColorRGB{0.0f, 0.9f, 0.5f});
    GLuint vao_grid = 0;
    GLuint vbo_grid_vertices = 0;
    send_vertices(&vao_grid, &vbo_grid_vertices, attr_position, grid_geometry.vertices);
    send_colors(&vao_grid, &vbo_grid_vertices, attr_color, grid_geometry.colors);

    // ----- X, Y, Z axis for visual debugging ----------------
    // X axis (GREEN) ; Y axis (RED); Z axis (BLUE)
    GLuint vao_axis = 0;
    GLuint vbo_axis_vertices = 0;
    GLuint vbo_axis_colors = 0;
    float axis_len = 5.0;

    // Each two points represents a unconnected line (GL_LINES)
    std::vector<Vertex3D> axis_vertices {
        // Line for X axis - from (0.0, 0.0, 0.0) to (axis_len, 0.0, 0.0)
          Vertex3D{0.0f, 0.0f, 0.0f}, Vertex3D{axis_len, 0.0f, 0.0f}
        // Line for Y axis
        , Vertex3D{0.0f, 0.0f, 0.0f}, Vertex3D{0.0f, axis_len, 0.0f}
        // Line for Z axis
        , Vertex3D{0.0f, 0.0f, 0.0f}, Vertex3D{0.0f, 0.0, axis_len}
    };

    auto axis_colors = std::vector<ColorRGB>{
          // Color of X axis line
          color_green, color_green
          // Color of Y axis line
         , color_red,  color_red
          // Color of Z axis line
         , color_blue, color_blue
    };

    send_vertices(&vao_axis, &vbo_axis_vertices, attr_position, axis_vertices);
    send_colors(&vao_axis,   &vbo_axis_colors,   attr_color,    axis_colors  );

    // ============== Set Shader Uniform Variables =============//
    //                                                          //

    int width, height;
    glfwGetWindowSize(window, &width, &height);
    // Window aspect ratio
    float aspect = static_cast<float>(width) / height;
    // Identity matrix
    const auto identity = glm::mat4(1.0);


    // Set projection matrix uniform variable
    glUniformMatrix4fv(u_proj, 1, GL_FALSE, glm::value_ptr(identity) );

    // ==== R E N D E R I N G - O B J E C T S ==========================//
    //                                                                  //

    RenderObject cube1{};
    cube1.vao        = vao_cube;
    cube1.u_model    = u_model;
    cube1.draw_type  = GL_QUADS;
    cube1.n_vertices = 24;
    // cube1.model = glm::scale(cube1.model, glm::vec3(0.5, 0.5, 0.5));
    cube1.model = glm::translate(cube1.model, glm::vec3(0.2, 1.20, +0.4));
    cube1.model = glm::rotate(cube1.model, glm::radians(45.0f), glm::vec3(1.0, 1.0, 0.0));

    // Call copy constructor (copy all data from cube1)
    RenderObject cube2 = cube1;
    cube2.model = glm::mat4(1.0);
    cube2.model = glm::translate(cube2.model, glm::vec3(-1.20, 1.2, 0.4));
    cube2.model = glm::scale(cube2.model, glm::vec3(1.5, 1.5, 1.5));
    // cube2.model = glm::rotate(cube2.model, glm::radians(60.0f), glm::vec3(1.0, 0.0, 1.0));


    // Grid in the plane containing the axis X and Y
    RenderObject grid_xy{};
    grid_xy.vao        = vao_grid;
    grid_xy.u_model    = u_model;
    grid_xy.draw_type  = GL_LINES; // Unconnected lines
    grid_xy.n_vertices = grid_geometry.vertices.size();
    grid_xy.model = glm::translate(grid_xy.model, glm::vec3(0.0, 1.5, -2.2));
    //grid_xy.model = glm::scale(grid_xy.model, glm::vec3(4.0, 4.0, 4.0));

    // Grid in the plane which contains the axis X and Z
    RenderObject grid_xz = grid_xy;
    grid_xz.model = glm::mat4(1.0);
    grid_xz.model = glm::rotate(grid_xz.model, glm::radians(-90.0f), glm::vec3(1.0, 0.0, 0.0));
     // grid_xz.model = glm::scale(grid_xz.model, glm::vec3(4.0, 4.0, 4.0));


    // Axis which is positioned at the origin of WCS (World Coordinate System)
    RenderObject axis_world;
    axis_world.u_model   = u_model;
    axis_world.vao       = vao_axis;
    axis_world.draw_type = GL_LINES;
    axis_world.n_vertices = axis_vertices.size();


    Camera camera(u_view, u_proj, width / height);

    //  ======= R E N D E R  - L O O P ============//
    //                                             //
    while ( !glfwWindowShouldClose(window) )
    {
        glClear(GL_COLOR_BUFFER_BIT  | GL_DEPTH_BUFFER_BIT);

        // ====== BEGIN RENDERING ============//

            // GL_CHECK( ::glutWireTeapot(0.50) );

            // ------ Draw Grid ----------
            axis_world.render();
            grid_xy.render();
            grid_xz.render();
            cube1.render();
            cube2.render();

        // ====== END RENDERING ==============//

        /* Swap front and back buffers */
        glfwSwapBuffers(window);
        /* Poll for and process events */
        glfwWaitEvents();

        if( glfwGetKey(window, 'Q' ) == GLFW_PRESS )
        {
             std::cout << " [TRACE] User typed Q =>> Shutdown program. Ok. " << '\n';
             break;
        }

        if( glfwGetKey(window, GLFW_KEY_RIGHT ) == GLFW_PRESS ){ camera.rotate_y(+10.0); }
        if( glfwGetKey(window, GLFW_KEY_LEFT  ) == GLFW_PRESS ){ camera.rotate_y(-10.0); }

        // Move camera up (positive Y axis)
        if( glfwGetKey(window, GLFW_KEY_UP  ) == GLFW_PRESS ){
            camera.cam_eye.y += 0.5;
            camera.update_view();
        }

        // Move camera down (negative Y axis)
        if( glfwGetKey(window, GLFW_KEY_DOWN  ) == GLFW_PRESS )
        {
            camera.cam_eye.y -= 0.5;
            camera.update_view();
        }

        // Rotate cube 1 in counter counter clockwise way around Y axis
        if( glfwGetKey(window, 'A'  ) == GLFW_PRESS )
        {
            cube1.model = glm::rotate( cube1.model, glm::radians(+10.0f), glm::vec3(0.0, 1.0, 0.0) );
        }
        // Rotate cube 1 in counter clockwise way around Y axis
        if( glfwGetKey(window, 'S'  ) == GLFW_PRESS )
        {
            cube1.model = glm::rotate( cube1.model, glm::radians(-10.0f), glm::vec3(0.0, 1.0, 0.0) );
        }

        // Rotate cube 1 in counter counter clockwise way around X axis (positive rotation)
        if( glfwGetKey(window, 'Z'  ) == GLFW_PRESS )
        {
            cube1.model = glm::rotate( cube1.model, glm::radians(+10.0f), glm::vec3(1.0, 0.0, 0.0) );
        }
        // Rotate cube 1 in counter counter clockwise way around X axis (negative rotation)
        if( glfwGetKey(window, 'X'  ) == GLFW_PRESS )
        {
            cube1.model = glm::rotate( cube1.model, glm::radians(-10.0f), glm::vec3(1.0, 0.0, 0.0) );
        }


    }

    glfwTerminate();
    return 0;

} // --- End of main() -----//


// ---------- S H A D E R - P R O G R A M S  -------------------------//
//

// Minimal vertex shader =>> Runs on the GPU and processes each vertex.
const char* code_vertex_shader = R"(
    #version 330 core

    layout ( location = 0)  in vec3 position;
    layout ( location = 1) in vec3 color;
    out vec3 out_color;
    uniform mat4 u_model;       // Model matrix
    uniform mat4 u_view;        // Camera's view matrix
    uniform mat4 u_projection;  // Camera's projection matrix

    void main()
    {
        gl_Position = u_projection * u_view * u_model * vec4(position, 1.0);

        // Forward to fragment shader
        out_color = color; // vec3(0.56, 0.6, 0.0);
    }

)";

// Fragment shader source code
const char* code_frag_shader = R"(
    #version 330

    // This color comes from Vertex shader
    in vec3 out_color;

    void main()
    {
        // Set vertex colors
        gl_FragColor =  vec4(out_color, 1.0);
        // gl_FragColor = vec4(0.3, 0.6, 0.0, 1.0);
    }
)";


    // ====== I M P L E M E N T A T I O N S ==========//

GLFWwindow*
make_glfwWindowi(int  width, int height, const char* title)
{

    glfwSetErrorCallback([](int error, const char* description)
                         { std::fprintf( stderr, " [GLFW ERROR] Error = %d ; Description = %s \n"
                                        , error, description);
                         });

    GLFWwindow* window = glfwCreateWindow(width, height, title, NULL, NULL);
    assert( window != nullptr && "Failed  to create Window");

    // OpenGL context
    glfwMakeContextCurrent(window);
    // Pain whole screen as black - dark screen colors are better
    // for avoding eye strain due long hours staring on monitor.
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    // Set - OpenGL Core Profile - version 3.3
    GL_CHECK( glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3) );
    GL_CHECK( glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3) );
    GL_CHECK( glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE) );

    GL_CHECK( glEnable(GL_COLOR_MATERIAL) );
    GL_CHECK( glEnable(GL_DEPTH_TEST)     );
    GL_CHECK( glEnable(GL_BLEND)          );

    return window;
}

void compile_shader(GLuint m_program, const char* code, GLenum type)
{
    GLint shader_id = glCreateShader( type );
    glShaderSource(shader_id, 1, &code, nullptr);
    glCompileShader(shader_id);

    GLint is_compiled = GL_FALSE;
    // Check shader compilation result
    glGetShaderiv(shader_id, GL_COMPILE_STATUS, &is_compiled);

    // If there is any shader compilation result,
    // print the error message.
    if( is_compiled == GL_FALSE)
    {
        GLint length;
        glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &length);
        assert( length > 0 );

        std::string out(length + 1, 0x00);
        GLint chars_written;
        glGetShaderInfoLog(shader_id, length, &chars_written, out.data());
        std::cerr << " [SHADER ERROR] = " << out << '\n';
        // Abort the exection of current process.
        std::abort();
    }

    glAttachShader(m_program, shader_id);
    glDeleteShader(shader_id);
    glLinkProgram(m_program);
    GLint link_status = GL_FALSE;
    glGetProgramiv(m_program, GL_LINK_STATUS, &link_status);
    assert( link_status != GL_FALSE );
    // glUseProgram(m_program);
}

// Upload buffer from main memory to GPU VBO
// =>> Parameters VAO, VBO are allocated by the caller.
void send_buffer( GLuint* pVao, GLuint* pVbo, GLsizei sizeBuffer
                , void* pBufffer, GLint   shader_attr, GLint size
                , GLenum type)
{
    assert(pVao != nullptr);
    assert(pVbo != nullptr);
    GLuint& vao = *pVao;
    GLuint& vbo = *pVbo;
    // Generate and bind current VAO (Vertex Array Object)
    if(vao == 0){ glGenVertexArrays(1, &vao); }
    glBindVertexArray(vao);
    // Generate and bind current VBO (Vertex Buffer Object)
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Upload data to current VBO buffer in GPU
    glBufferData(GL_ARRAY_BUFFER, sizeBuffer, pBufffer, GL_STATIC_DRAW);
    glEnableVertexAttribArray(shader_attr);
    // Set data layout - how data will be interpreted.
    glVertexAttribPointer(shader_attr, size, type, GL_FALSE, 0, nullptr);
    // ------ Disable Global state set by this function -----//
    // Unbind VAO
    glBindVertexArray(0);
    // Unbind VBO
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Disable current shader attribute
    glDisableVertexAttribArray(shader_attr);

}


Geometry
make_grid_geometry(size_t n_grid, float dx, const ColorRGB& color)
{
    std::vector<Vertex3D> grid_vertices;
    std::vector<ColorRGB> grid_colors ;

    float dy = dx;
    float grid_w  = n_grid * dx;
    float grid_h  = n_grid * dy;

    // Draw horizontal lines - parallel to X axis
    for(size_t n = 0; n < 2 * n_grid; n++)
    {
        // Line Vertex A (BEGIN)
        grid_vertices.push_back( Vertex3D{ -grid_w, dy * n - grid_h, 0 } );
        // Color of vertex A
        grid_colors.push_back(color);
        // Line Vertex B (END)
        grid_vertices.push_back( Vertex3D{ +grid_w, dy * n - grid_h, 0  } );
        // Color of vertex B
        grid_colors.push_back(color);
    }

     // Draw horizontal lines - parallel to Y axis
    for(size_t n = 0; n < 2 * n_grid; n++)
    {
        // Line point A (BEGIN)
        grid_vertices.push_back( Vertex3D{ dx * n - grid_w, -grid_h, 0 } );
        // Color of Vertex A
        grid_colors.push_back(color);
        // Line pojnt B (END)
        grid_vertices.push_back( Vertex3D{ dx * n - grid_w, +grid_h, 0  } );
        // Color of Vertex B
        grid_colors.push_back(color);

    }

    return Geometry{ grid_vertices, grid_colors };

}

Geometry
make_cube_geometry(float w)
{
    // Array of cube vertex coordinates (X, Y)
    auto vertices = std::vector<Vertex3D> {
        // Top face {y = w}
        { w, w, -w}
      , {-w, w, -w}
      , {-w, w,  w}
      , { w, w,  w}

      // Bottom face , {y = -w}
      , { w, -w,  w}
      , {-w, -w,  w}
      , {-w, -w, -w}
      , { w, -w, -w}

      // Front face  , {z = w}
      , { w,  w, w}
      , {-w,  w, w}
      , {-w, -w, w}
      , { w, -w, w}

      // Back face , {z = -w}
      , { w, -w, -w}
      , {-w, -w, -w}
      , {-w,  w, -w}
      , { w,  w, -w}

      // Left face , {x = -w}
      , {-w,  w,  w}
      , {-w,  w, -w}
      , {-w, -w, -w}
      , {-w, -w,  w}

      // Right face , {x = w}
      , {w,  w, -w}
      , {w,  w,  w}
      , {w, -w,  w}
      , {w, -w, -w}
    };

    // std::fprintf(stderr, " [TRACE] Number of cube vertices = %zu \n", cube_vertices.size() );

    auto colors = std::vector<ColorRGB> {
           color_red
        ,  color_red
        ,  color_red
        ,  color_red

        , color_blue
        , color_blue
        , color_blue
        , color_blue

        , color_green
        , color_green
        , color_green
        , color_green

        , color_gray
        , color_gray
        , color_gray
        , color_gray

        , color_dark_green
        , color_dark_green
        , color_dark_green
        , color_dark_green

        , color_dark_blue
        , color_dark_blue
        , color_dark_blue
        , color_dark_blue
    };

    return Geometry{ vertices, colors };
}

Building on Linux

$ cmake -H. -B_build_linux -DCMAKE_BUILD_TYPE=Debug
$ cmake --build _build_linux --target

$ ls _build_linux/
CMakeCache.txt  cmake_install.cmake  draw3d-cube*
CMakeFiles/     _deps/               Makefile

Run the application:

$ >> _build_linux/draw3d-cube
[TRACE] vao_cube = 1 

Cross-compiling for Windows and testing with wine

STEP 1: Cmake configuration step

$ docker run -it --rm --user=$(id -u):$(id -g)  \
       --volume=$PWD:/cwd --workdir=/cwd  dockcross/windows-static-x64 \
       cmake -H. -B_build_cross -DCMAKE_BUILD_TYPE=Debug

STEP 2: Cmake building step

$ docker run -it --rm --user=(id -u):(id -g)  \
    --volume=$PWD:/cwd --workdir=/cwd  dockcross/windows-static-x64 \
    cmake --build _build_cross --target

STEP 3: Check compiled files

$ >> ls _build_cross/
CMakeCache.txt  cmake_install.cmake  draw3d-cube.exe*  Makefile
CMakeFiles/     _deps/               glew32.dll*

STEP 4: Run the application with Wine

 $ file _build_cross/draw3d-cube.exe 
_build_cross/draw3d-cube.exe: PE32+ executable (console) x86-64, for MS Windows

 $ file _build_cross/glew32.dll 
_build_cross/glew32.dll: PE32+ executable (DLL) (console) x86-64, for MS Windows

$ wine _build_cross/draw3d-cube.exe

1.23 3D - WebGL 3D Scene with grid and cube

By using WebGL API, it is possible to learn and prototype computer graphics without the complexity of dealing with building systems, compilation, memory management and C or C++ programming. This code takes advantage of the ubiquity and availability of JavaScript and WebGL on every modern web browser for demonstrating the usage of OpenGL and WebGL APIs for rendering a simple 3D scene containing a camera, grid, cube and tetrahedron. The sample WebGL application uses no external dependencies and implements everything from scratch which makes it easier to reason about the code and about 3D programming concepts.

Since the code is written in JavaScript and html, it is possible to experiment and play with webGL APIs used in the code by opening the next link.

In the live example, there are the following controls:

  • 'a' key => rotates the camera view upwards (forward vector)
  • 's' key => rotates the camera view downwards (forward)
  • arrow up => Moves the camera position (eye vector) in the direction that camera is looking at (forward vector).
  • arrow down => Moves the camera position (eye vector) in the opposite direction that the camera is looking at.
  • arrow right => Rotates the camera around its up vector (orientation).
  • arrow left => Rotates the camera in the opposite direction around its up vector (orientation).
  • 'j' => Move the camera in the positive direction of Y axis
  • 'k' => Move the camera in the negative direction of Y axis.
  • 'q' => Rotates the cube.
  • 'r' => Reset camera position (eye vector) and view (forward vector)

Notes:

  • The class Matrix4x4 that represents a mutable 4x4 matrix column-major matrix is inspired by the c++ class QMatrix4x4 class from Qt framework.
  • The Quaternion class was inspired by C++ class QQuaternion from Qt framework.
  • The class Orientation was inspired by the C++ class FTransform from Unreal game engine. The idea was copied from public information available in the documentation.
  • WebGL and OpenGL ES only support matrices in column-major order memory layout. Those APIs do not support matrices in row-major order.
  • WebGL, which is based on OpenGL ES only supports the retained mode. WebGL does not support immediate mode. Therefore there is no equivalent in WebGL to the old and deprecated OpenGL retained mode APIs, including glVertex3f(), glBegin(), glEnd(), glColor(), glMaterial(), glRotate(), glTranslate(), glFrustum() and many other deprecated OpenGL subroutines.
  • In modern OpenGL and specially in OpenGL ES and WebGL, it is only possible to draw primitive forms such as lines, points and triangles, any complex shape requires a good enough amount of primitive shapes in order to look like smooth and realistic.

Screenshots

opengl-webgl-cube3d-1.png

opengl-webgl-cube3d-2.png

More about WebGL

Complete Html Code (File: webgl2.html)

<!DOCTYPE html>
<html> 
   <head> 
       <title>WebGL 3D Scene</title>
    </script>
   </head>

   <body>
       <h1>WebGL 3D Scene with Cube</h1>
       OpenGL version:  <label id="output1"></label>          <br/>
       Shading language version: <label id="output2"></label> <br/> 
       Camera Position: <label id="output-camera"></label> <br/>
       <canvas id="glCanvas" width="600" height="480">
   </body>
    <script>
// eye = [3, 10, 20]; at = [50, 25, 10]; up = [0, 1, 0];

// RGB color constants (Red, Gree, Blue) (R, G, B) tuples
const COLOR_RED        = [1.0, 0.0, 0.0];
const COLOR_GREEN      = [0.0, 1.0, 0.0];
const COLOR_BLUE       = [0.0, 0.0, 1.0];
const COLOR_YELLLOW    = [ 0.80,  1.000, 0.100 ];
const COLOR_GRAY       = [ 0.47,  0.390, 0.380 ];
const COLOR_DARK_GREEN = [ 0.027, 0.392, 0.050 ];
const COLOR_DARK_BLUE  = [ 0.109, 0.066, 0.411 ];   

  ... ... ..... . ... .... ... ... ... ... 
  ... ... ..... . ... .... ... ... ... ... 

    </script>

  </html> 

JavaScript part:

// eye = [3, 10, 20]; at = [50, 25, 10]; up = [0, 1, 0];

// RGB color constants (Red, Gree, Blue) (R, G, B) tuples
const COLOR_RED        = [1.0, 0.0, 0.0];
const COLOR_GREEN      = [0.0, 1.0, 0.0];
const COLOR_BLUE       = [0.0, 0.0, 1.0];
const COLOR_YELLLOW    = [ 0.80,  1.000, 0.100 ];
const COLOR_GRAY       = [ 0.47,  0.390, 0.380 ];
const COLOR_DARK_GREEN = [ 0.027, 0.392, 0.050 ];
const COLOR_DARK_BLUE  = [ 0.109, 0.066, 0.411 ];


// Normalize a vector 3x1 column vector (3 rows and 1 column)  
function normalize(vector)
{
  let [x, y, z] = vector;
  let norm = Math.sqrt( x * x + y * y + z * z);
  return [ x / norm, y / norm, z / norm ];
}

// Computes the dot product (aka scalar) product between two vectors  
function dot(vectorA, vectorB)
{
  let [xa, ya, za] = vectorA;
  let [xb, yb, zb] = vectorB;
  return xa * xb  + ya * yb + za * zb;
}

// Computs the cross product between two vectors 
function cross(vectorA, vectorB)
{
  let [xa, ya, za] = vectorA;
  let [xb, yb, zb] = vectorB;
  return [ ya * zb - za * yb, za * xb - xa * zb, xa * yb - ya * xb ];
}

// Difference between two vectors 
function diff(vectorA, vectorB)
{
  let [xa, ya, za] = vectorA;
  let [xb, yb, zb] = vectorB;
  return [xa - xb, ya - yb, za - zb];
}

class Quaternion
{
  constructor(w, x, y, z)
  {
    // Default is the unit quaternion 
    this._quat = [w, x, y, z];
  }

  /** @param {number} w
  /*  @param {number} x 
  /*  @param {number} y 
  /*  @param {number} z 
   */ 
  static create(w, x, y, z)
  {
    return new Quaternion(w, x, y, z);
  }

  /** Create unit quaternion  */
  static createUnit()
  {
    return new Quaternion(1.0, 0.0, 0.0, 0.0);
  }

  /** Create a imaginary quaternion */
  static createFromVector(x, y, z)
  {
    return new Quaternion(0.0, x, y, z);
  }

  /** Create quaternion represeting a rotation around some axis 
   * @param {number}   angle - Rotation angle in degrees 
   * @param {number[]} axis  - Rotation axis
   */
  static createFromAxisAngle(angle, axis)
  {
    let [nx, ny, nz] = normalize(axis);
    // Angle in radians 
    let t = angle / 180.0 * Math.PI / 2;
    let C = Math.cos(t);
    let S = Math.sin(t);
    return Quaternion.create(C, S * nx, S * ny, S * nz);
  }

  negate()
  {
    let [w, x, y, z] = this._quat;
    return new Quaternion(-w, -x, -y, -z);
  }

  conjugate()
  {
    let [w, x, y, z] = this._quat;
    return new Quaternion(w, -x, -y, -z);
  }

  // Note: It is assumed that the current quaternion is a unit quaternion 
  rotateVector(v)
  {
    let qa = this;
    let qb = Quaternion.createFromVector(...v);
    // r1 = qa * qb 
    let r1 = qa.multiply(qb);
    let r2 = r1.multiply(qa.conjugate());
    // r2 = qa * qb * qa.conjugage()
    //let b = q.multiply( qa.conjugate() );
    return r2.getVector();
  }

  /** Get imaginary part of a quaternion */
  getVector()
  {
    let [w, x, y, z] = this._quat;
    return [x, y, z];
  }

  /** Turn the quaternion into a homogeneous rotation matrix */
  toMatrix()
  {
    // The quaternion is assumed to be a unit quaternion 
    // otherwise the matrix result will be invalid
    let [a, b, c, d] = this._quat;
    let m = new Float32Array(16);
    m[0] = a * a - c * c - d * d;
    m[1] = 2 * (b * c + a * d);
    m[2] = 2 * (b * d - a * c);
    m[3] = 0.0;

    m[4] = 2 * (b * c - a * d);
    m[5] = a * a - b * b + c * c -  d * d;
    m[6] = 2 * (c * d + a * b);
    m[7] = 0.0;

    m[8] = 2 * (b * d + a * c);
    m[9] = 2 * (c * d - a * b);
    m[10] = a * a - b * b - c * c + d * d;
    m[11] = 0;

    m[12] = 0.0;
    m[13] = 0.0;
    m[14] = 0.0;
    m[15] = 1.0;

    return m;
  }

  /** Multiply a quaternion for another and returns a new quaternion 
   * Note: JavaScript lacks operator overloading
   * Note: Quaternion multiplication is not commutative 
   * @param {Quaternio} q
   **/
  multiply( q )
  {
    let [w1, x1, y1, z1] = this._quat;
    let [w2, x2, y2, z2] = q._quat; 
    let result = [
          w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2 
        , x1 * w2 + w1 * x2 - z1 * y2 + y1 * z2
        , y1 * w2 + z1 * x2 + w1 * y2 - x1 * z2 
        , z1 * w2 - y1 * x2 + x1 * y2 + w1 * z2  
    ];
    return new Quaternion(...result);
  }

}



class Matrix4x4 
{

  constructor()
  {
      this.mat = new Float32Array(16);
      this.mat[0] = 1.0;
      this.mat[5] = 1.0;
      this.mat[10] = 1.0;
      this.mat[15] = 1.0;
  }

  static create()
  {
    return new Matrix4x4();
  }

  at(i, j) 
  {
    return this.mat.at(j + 4 * i);
  }

  setToIdentity() 
  {
      this.mat = new Float32Array(16);
      this.mat[0] = 1.0;
      this.mat[5] = 1.0;
      this.mat[10] = 1.0;
      this.mat[15] = 1.0;
      return this;
  }

  // this.matrix = this.matrix * translateMatrix(x, y, z)
  /**
   *  @param {number} x 
   *  @param {number} y 
   *  @param {number} z 
   */ 
  translate(x, y, z)
  {
    let matrix = [   1, 0, 0, 0
                   , 0, 1, 0, 0 
                   , 0, 0, 1, 0 
                   , x, y, z, 1 
                  ];
    this.multiply(matrix);
    return this;
  }

  translateV(vector)
  {
    this.translate(vector[0], vector[1], vector[2]);
    return this;
  }

  /**
   *  @param {number} sx 
   *  @param {number} sy 
   *  @param {number} sz 
   */ 
  scaleV(sx, sy, sz)
  {
    let matrix = [  sx,  0,  0, 0 
                   , 0, sy,  0, 0 
                   , 0,  0, sz, 0 
                   , 0,  0,  0, 1 
                  ];
    this.multiply(matrix);
    return this;
  }

  /**
   *  @param {number} k
   */ 
  scale(k)  
  {
     let m = new Float32Array(16);
     m[0] = k; 
     m[5] = k; 
     m[10] = k; 
     m[15] = 1.0; 
    this.multiply(m);
    return this;
  }

  // Rotation around arbitrary axis (vector) designated by X, Y, Z
  // the angle is in radians 
  // this.matrix = this.matrix * rotate(angle, x, y, z)
  rotate(angle, x, y, z)
  {
    let r = new Float32Array(16);
    let t = angle * Math.PI / 180.0;
    let C = Math.cos(t);
    let S = Math.sin(t);
    let K = 1 - C ;
    r[0] = x*x + (1 - x*x)*C;
    r[4] = x*y*K - z*S;
    r[8] = x*z*K + y*S;
    r[12] = 0;
    r[1] = x*y*K + z*S;
    r[5] = y*y + (1 - y*y)*C
    r[9] = -x*S + y*z*K;
    r[13] = 0;
    r[2] = x*z*K - y*S;
    r[6] = x*S + y*z*K;
    r[10] = z*z + (1 - z*z)*C;
    r[14] = 0;
    r[3] = 0;
    r[7] = 0;
    r[11] = 0;
    r[15] = 1;
    this.multiply(r);
    return this;
  }

  /** Rotate around Z axis 
   * @param {number} angle - Rotation angle in degrees 
   */  
  rotateZ(angle)
  {
    // let r = new Float32Array(16);
    let r = Array(16).fill(0.0);
    let t = angle * Math.PI / 180.0;
    let C = Math.cos(t);
    let S = Math.sin(t);
    r[0] = C; r[4] = -S;
    r[1] = S; r[5] =  C;
    r[10] = 1; r[15] = 1;

    this.multiply(r);
    //this.mat = r;
    return this;
  }

  /** Rotate around some axis 
   * @param  {number}    angle  - rotation angle in degrees
   * @param  {number[]}  axis   - rotation axis as an array of 3 numbers 
   */ 
  rotateAxis(angle, axis)
  {
    let [x, y, z] = axis;
    this.rotate(angle, x, y, z);
    return this;
  }

  /** Rotate current matrix by multiplying it by the quaternion rotation matrix 
   *  this.matrix = this.matrix * quaternion.RotMatrix 
   * @param {Quaternion} q 
   **/
  rotateQuaternion(q)
  {
    let rotmatrix = q.toMatrix();
    this.multiply(rotmatrix);
    return this;
  }

  setLookAt(eye, at, up)
  {
    let Z = normalize( diff(eye, at) );
    let X = normalize( cross(up, Z) );
    let Y = normalize( cross(Z, X) );
    let m = new Float32Array(16);
    m[0]  = X[0]; m[4]  = X[1]; m[8]  = X[2];  m[12]  = -dot(X, eye);
    m[1]  = Y[0]; m[5]  = Y[1]; m[9]  = Y[2];  m[13]  = -dot(Y, eye);
    m[2]  = Z[0]; m[6]  = Z[1]; m[10] = Z[2];  m[14] = -dot(Z, eye);
    m[3] = 0;     m[7] = 0;     m[11] = 0;     m[15] =  1; 
    this.mat = m;
    return this;
  }

  // FOV angle in degrees 
  setPerspective(FOV, aspect, zNear, zFar)
  {
    // Angle in radians
    let t = Math.PI * FOV / 180.0; 
    let k = Math.tan(t / 2);
    let m = new Float32Array(16);
    m[0] = 1 / ( aspect * k ); 
    m[5] = 1 / k;
    m[10] = (zNear + zFar) / (zNear - zFar);
    m[14] = 2 * zNear * zFar / (zNear - zFar);
    m[11] = -1; 
    this.mat = m; 
    return this;
  }

  // In place matrix multiplication 
  multiply( matrix )
  {
    let a = this.mat;
    let b = matrix;
    let c = new Float32Array(16);

  // Matrix multiplication algorithm of column-major matrices borrowed from 
  // https://github.com/yycho0108/Abstraction/blob/master/gl-matrix.min.js
   var d = a[0], e = a[1], g = a[2], f = a[3], h = a[4], i = a[5], j = a[6], k = a[7]
          , l = a[8], o = a[9], m = a[10], n = a[11], p = a[12], r = a[13], s = a[14];

    a = a[15];
    var A = b[0], B = b[1], t = b[2], u = b[3], v = b[4], w = b[5], x = b[6], y = b[7], z = b[8]
        , C = b[9], D = b[10], E = b[11], q = b[12], F = b[13], G = b[14];

    b = b[15];
    c[0] = A * d + B * h + t * l + u * p;
    c[1] = A * e + B * i + t * o + u * r;
    c[2] = A * g + B * j + t * m + u * s;
    c[3] = A * f + B * k + t * n + u * a;
    c[4] = v * d + w * h + x * l + y * p;
    c[5] = v * e + w * i + x * o + y * r;
    c[6] = v * g + w * j + x * m + y * s;
    c[7] = v * f + w * k + x * n + y * a;
    c[8] = z * d + C * h + D * l + E * p;
    c[9] = z * e + C * i + D * o + E * r;
    c[10] = z *
        g + C * j + D * m + E * s;
    c[11] = z * f + C * k + D * n + E * a;
    c[12] = q * d + F * h + G * l + b * p;
    c[13] = q * e + F * i + G * o + b * r;
    c[14] = q * g + F * j + G * m + b * s;
    c[15] = q * f + F * k + G * n + b * a;
    this.mat = c;
  }  

}


function assertNotUndefined(variableName, variable)
{
  if( typeof variable == "undefined" )
  {
    alert(` [ASSERT ERROR] ${variableName} is undefined.`);
  }
}


    // Based on:  https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html
function compileShaderProgram(gl, vertexShaderCode, fragmentShaderCode) 
{
    function createShader(gl, type, source) 
    {
      assertNotUndefined("gl", gl);
      var shader = gl.createShader(type);
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
      var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
      if (success) {
        return shader;
      }

      console.log(" [SHADER ERROR] = \n" + source);
      console.log(" [SHADER ERROR] "  + gl.getShaderInfoLog(shader) );
      // alert(" [SHADER ERORR]  " + gl.getShaderInfoLog(shader) );
      gl.deleteShader(shader);
    }

    let vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderCode);

    let fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderCode);
    console.log(" [TRACE] Compiled fragmentShader Ok.");

    var program = gl.createProgram();
    assertNotUndefined("program", program);
    gl.attachShader(program, vertexShader);
    console.log(" [TRACE] Attach vertex shader Ok");
    gl.attachShader(program, fragmentShader);
    console.log(" [TRACE] Attach fragment shader Ok");
    gl.linkProgram(program);
    var success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (success) {
      return program;
    }

    console.error(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
}


function resizeCanvasToDisplaySize(canvas, multiplier) 
{
    multiplier = multiplier || 1;
    const width  = canvas.clientWidth  * multiplier | 0;
    const height = canvas.clientHeight * multiplier | 0;
    if (canvas.width !== width ||  canvas.height !== height) {
      canvas.width  = width;
      canvas.height = height;
      return true;
    }
    return false;
}

// Vertex Buffer Object abstraction 
class Vbuffer 
{
  constructor(gl, attr_position, attr_color)
  {
    this._gl = gl;
    this._vbo = gl.createBuffer();   
    this._data = new Float32Array(1);
    this._attr_position = attr_position;
    this._attr_color = attr_color;
    this._vertices = 0;
  }

  unbind()
  {
    let gl = this._gl;
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  }

  // Release the VBO from the GPU 
  free()
  {
    let gl = this._gl;
    gl.deleteBuffer(this._vbo);
  }

  setData(data, ncomps = 3, stride = 0, istride = 0 )
  {
    let gl = this._gl;
    this._ncomps = ncomps;
    this._stride = stride; 
    this._istide = 0;
    this._vertices = data.length;
    gl.bindBuffer( gl.ARRAY_BUFFER, this._vbo );
    this._data = new Float32Array(data);
    // Send data to GPU
    gl.bufferData(gl.ARRAY_BUFFER, this._data, gl.STATIC_DRAW);
  }

  drawTriangles()
  {
    let gl = this._gl;
    this.draw(gl.TRIANGLES);
  }

  drawLines()
  {
    let gl = this._gl;
    this.draw(gl.LINES);
  }

  drawLineLoop()
  {

    let gl = this._gl;
    this.draw(gl.LINE_LOOP);
  }

  draw(drawType)
  {
    let gl = this._gl;
    gl.bindBuffer( gl.ARRAY_BUFFER, this._vbo );

    // Position data layout for each vertex 
    gl.vertexAttribPointer(  this._attr_position  // Shader attribute 
                           , 3                    // Number of components, coordinates
                           , gl.FLOAT             // Type of each componente
                           , false                // Normalized flag 
                           , 24                   // Stride  = sizeof(Vertex) = sizeof(Position) + sizeof(Color) = 
                                                  // Stride  = 3 * sizeof(Float in bytes) + 3 * sizeof(Float in bytes) 
                                                  // Stride  = 3 * 4 + 3 * 4 = 24  
                           , 0                    // offset  
                          );

    // Color data layout for each vertex  
    gl.vertexAttribPointer(  this._attr_color     // Shader attribute 
                           , 3                    // Number of components, coordinates
                           , gl.FLOAT             // Type of each componente
                           , false                // Normalized flag 
                           , 24                   // Stride 
                           , 12                   // offset - offset of color attribute = 3 * sizeof(float) =  3 * 4 = 12 
                          );


    // Each vertex has 6 coordinates - 3 coordinates for position and 3 coordinates for color
    gl.drawArrays(drawType, 0, this._vertices / 6);

    // Unbind VBO and disable global state 
    gl.bindBuffer( gl.ARRAY_BUFFER, null);
  }
}

class Camera
{
  constructor(gl, uniform_view, uniform_proj, uniform_eye)
  {
    // Current location of camera in world coordinates
    this.eye     = [  2.0, 4.0,  5.0 ];
    // Direction to where camera is looking at.
    this.forward = [ -1.0, 0.0, -1.0 ];
    // UP vector - camera orientation (default Y axis)
    this.up      = [  0.0, 1.0,  0.0 ];

    // Field of view angle stated in degrees  
    this.fov_angle = 60.0; 
    // Aspect ratio  
    this.aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
    // Far 
    this.zFar = 100.0;
    // Near 
    this.zNear = 0.1;

    this.gl = gl;
    this.uniform_view = uniform_view;
    this.uniform_proj = uniform_proj;
    this.uniform_eye  = uniform_eye;

    this.updateView();

    console.log(" [TRACE] Crated Camera Ok.");
  }

  // Reset camera to initial position
  reset()
  {
    // Current location of camera in world coordinates
    this.eye     = [  2.0, 4.0,  5.0 ];
    // Direction to where camera is looking at.
    this.forward = [ -1.0, 0.0, -1.0 ];
    // Direction to where camera is looking at.
    this.up      = [  0.0, 1.0,  0.0 ];
    // Field of view angle stated in radians. It is by default 60 degrees.
    this.fov_angle = 60.0; //* Math.PI / 180.0;
  }

  // Rotate around camera's Up vecto.
  rotateYaw(angle)
  {
    let q = Quaternion.createFromAxisAngle(angle, this.up);
    this.forward = q.rotateVector(this.forward);
    this.updateView();
  }

  // Rotate around camera's pitch axis (X axis) elevate camera view.
  rotatePitch(angle)
  {
    let axis = cross(this.up, this.forward);
    let q = Quaternion.createFromAxisAngle(angle, axis);
    this.forward = q.rotateVector(this.forward);
    this.updateView();
  }

  // Move camera in Y axis 
  moveCameraUpDow(factor)
  {
    // Increase of decrease Y component of Eye vector
    this.eye[1] = this.eye[1] + factor;
    this.updateView();
    return this;
  }

  // Make the vector forward parallel to the plane XZ
  parallelXZ()
  {
    this.forward[1] = 0.0; 
    this.updateView();
    return this;
  }

     // Move at forward vector direction (to where camera is looking at).
  moveForward(factor)
  {
      // this->eye = this->eye + factor * this->forward;
      let [ex, ey, ez] = this.eye; 
      let [fx, fy, fz] = this.forward;
      this.eye = [ex + factor * fx, ey + factor * fz, ez + factor * fz];
      this.updateView();
      return this
  }

  // Set camera position in world coordinates 
  setPosition(position)
  {
    this.eye = position; 
    this.updateView();
    return this;
  }

  setPositonXYZ(x, y, z)
  {
    this.setPosition([x, y, z]);
    return this;
  }

  lookAt(at)
  {
    // Set position to where camera is looking at.
    this.forward = diff(at, this.eye) ;
    this.updateView();
    return this;
  }

  lookAtXYZ(x, y, z)
  {
    this.lookAt([x, y, z]);
    return this;
  }


  updateView() 
  {
    // Perspective/Projection matrix 
    // Tp = glm::perspective( fov_angle, aspect, zNear, zFar );
    const Tp = new Matrix4x4(); 
    Tp.setPerspective(this.fov_angle, this.aspect, this.zNear, this.zFar);
    // console.log(" [TRACE] Tp = ", Tp);

    // View matrix 
    // auto Tview = glm::lookAt(eye, cam_at, up);
    const Tv = new Matrix4x4();
    // Location where camera is looking at in world coordinates 
    const at = [ this.eye[0] + this.forward[0], this.eye[1] + this.forward[1], this.eye[2] + this.forward[2] ];
    Tv.setLookAt(this.eye, at, this.up);
    // console.log(" [TRACE] Tv = ", Tv);

    // glUniformMatrix4fv(shader_uniform_view, 1, GL_FALSE, glm::value_ptr(Tview) );
    this.gl.uniformMatrix4fv(this.uniform_view, false, Tv.mat);

    // glUniformMatrix4fv(shader_uniform_proj, 1, GL_FALSE, glm::value_ptr(Tproj) );
    this.gl.uniformMatrix4fv(this.uniform_proj, false, Tp.mat);

  }

  setIdentity()
  {
    let m = new Matrix4x4();
    this.gl.uniformMatrix4fv(this.uniform_view, false, m.mat);
    this.gl.uniformMatrix4fv(this.uniform_proj, false, m.mat);
  }

}


/** Encapsulates the position, scale and  rotation of an object in the space */
class Orientation
{
  constructor()
  {
    // Default scale 1.0 - original object size 
    this._scale     = 1.0; 
    // Default position at origin 
    this._position  = [0.0, 0.0, 0.0];
    // Unit quaternion 
    this._rotation = Quaternion.create(1.0, 0.0, 0.0, 0.0);
    this._is_changed = false;
    this._model = Matrix4x4.create()
                          .translateV(this._position)
                          .scale(this._scale)
                          .rotateQuaternion(this._rotation)
  }

  reset()
  {
    this._scale     = 1.0; 
    this._position  = [0.0, 0.0, 0.0];
    this.__rotation = Quaternion.create(1.0, 0.0, 0.0);
    this._is_changed = true;
  }

  setPosition(x, y, z)
  {
    this._position = [x, y, z];
    this._is_changed = true;
  }

  translate(dx, dy, dz)
  {
      this._position[0] += dx;
      this._position[1] += dy;
      this._position[2] += dz;
      this._is_changed = true;
      return this;
  }

  setScale(scale)
  {
      this.scale = scale;
      this.is_changed = true;
      return this;
  }

  rotateZ(angle)
  {
    // console.log(" [TRACE] Rotating angle = ", angle);
    // this.angle = this.angle + angle;
    this.rotate(angle, [0.0, 0.0, 1.0]);
    this._is_changed = true;
    return this;
  }

  rotate(angle, axis)
  {
    let q = Quaternion.createFromAxisAngle(angle, axis);
    this._rotation = this._rotation.multiply(q);
    this._is_changed = true;
    return this;
  }

  getModel()
  {
    if( this._is_changed )
    {
        this._is_changed = false;
        this._model.setToIdentity()
                  .translateV(this._position)
                  .scale(this._scale)
                  .rotateQuaternion(this._rotation);
    }
    return this._model;
  }

}

class Entity 
{
  constructor(gl, vbo, u_model, drawType)
  {
    this.gl = gl;
    this.vbo = vbo; 
    this.u_model = u_model;

    this.is_visible = true;

    // Draw Triangles by default 
    this.drawType = drawType; 
    this.orientaton = new Orientation();
  }

  setDrawTriangles() { this.drawType = this.gl.TRIANGLES; return this; }
  setDrawLines() { this.drawType = this.gl.LINES;         return this; }
  setDrawLineLoop() { this.drawType = this.gl.LINE_LOOP;  return this; }
  setVisible(flag) { this.is_visible = flag;              return this; }
  toggleVisible() { this.is_visible = !this.is_visible;         return this; }
  setPosition(x, y, z) { this.orientaton.setPosition(x, y, z);  return this; }
  translate(dx, dy, dz){ this.orientaton.translate(dx, dy, dz); return this; }
  setScale(k){ this.orientaton.setScale(k);                     return this; }
  rotateZ(angle){ this.orientaton.rotateZ(angle);               return this; }
  rotate(angle, axis){ this.orientaton.rotate(angle, axis);     return this; }

  draw() 
  {
    let model = this.orientaton.getModel();
    if( this.is_visible )
    {
       this.gl.uniformMatrix4fv(u_model, false, model.mat); 
       this.vbo.draw(this.drawType);
    }
  }
}

class EntityFactory 
{

  /** @param {Vbuffer} - vbo */
  constructor(gl, vbo, u_model)
  {
    this.gl = gl; 
    this.vbo = vbo;
    this.u_model = u_model;
    this.drawType = gl.TRIANGLES;
  }

  setDrawTriangles()
  {
    this.drawType = this.gl.TRIANGLES;
    return this;
  }

  setDrawLines()
  {
    this.drawType = this.gl.LINES;
    return this;
  }

  setDrawLineLoop()
  {
    this.drawType = this.gl.LINE_LOOP;
    return this;
  }

  create()
  {
      return new Entity(this.gl, this.vbo, this.u_model, this.drawType);
  }
}

    // ----- R E N D E R I N  G ------------------//


// Vertex Shader code => Performs vertex coordinate transforms in the GPU 
let code_shader_vert =` 
  // precision mediump float;

  // Note: keyword attribute is deprecated 
  attribute vec3 position;
  attribute vec3 color; 

  varying vec3 out_color;

  uniform vec3 u_eye;         // Cameras's position in space
  uniform mat4 u_model;       // Model matrix 
  uniform mat4 u_view;        // Camera's view matrix    
  uniform mat4 u_projection;  // Camera's projection matrix 

  void main(){

    gl_Position = u_projection * u_view * u_model * vec4(position, 1.0);

    // Forward to fragment shader 
    out_color = color; 

  }
`;

// Fragment shader code => Sets color, lights and illumination 
let code_shader_frag = `
   // precision mediump float;

   // varying low vec3 out_color;
   varying lowp vec3 out_color;

    void main()
    {
      gl_FragColor = vec4(out_color, 1.0);
      //gl_FragColor = vec4(0, 0, 1.0, 1.0);
    }
`;

  // ----------- Rendering Starts Here -----------//


const canvas = document.querySelector("#glCanvas");
const gl = canvas.getContext("webgl");

if(  gl == null ){
  alert(" [ABORT] Unable to initialized WebGL");
}

console.log(" [DEBUG] OpenGL version = " + gl.getParameter(gl.VERSION) );
console.log(" [DEBUG] Shading language version = " + gl.getParameter(gl.SHADING_LANGUAGE_VERSION));

document.querySelector("#output1").textContent = gl.getParameter(gl.VERSION);
document.querySelector("#output2").textContent =  gl.getParameter(gl.SHADING_LANGUAGE_VERSION);

let prog =  compileShaderProgram(gl, code_shader_vert, code_shader_frag);
gl.useProgram(prog);

// Uniform variables 
let u_proj       = gl.getUniformLocation( prog, "u_projection");
let u_view       = gl.getUniformLocation( prog, "u_view");
let u_model      = gl.getUniformLocation( prog, "u_model");
let u_camera_eye = gl.getUniformLocation( prog, "u_eye");

// Attribute Locations 
let attr_position = gl.getAttribLocation(prog, "position");
let attr_color    = gl.getAttribLocation(prog, "color");

let identity = new Matrix4x4();

// Initialize with identity matrix 
gl.uniformMatrix4fv(u_model, false, identity.mat); 
gl.uniformMatrix4fv(u_view,  false, identity.mat); 
gl.uniformMatrix4fv(u_proj,  false, identity.mat);


function buildGridPlaneXY(gl, attr_position, attr_color, n , delta)
{
    let vertices = [];

    let xmax = n * delta; 
    let ymax = n * delta;

    // Draw lines in plane XY parallel to line X 
    for(let i = 0; i < n; i++)
    {
      // Position of first vertex 
      let position1 = [0.0, i * delta, 0.0 ];
      // Positon of first line vertex   
      vertices.push(...position1);    
      // Color of second line vertex 
      vertices.push(...COLOR_BLUE);   

      // Position of second vertex  
      let position2 = [xmax, i * delta, 0.0];
      // Positon of first line vertex   
      vertices.push(...position2);    
      // Color of second line vertex 
      vertices.push(...COLOR_BLUE);   
    }

    // Draw lines in plane XY parallel to line Y
    for(let j = 0; j < n; j++)
    {
      // Position of first vertex 
      let position1 = [j * delta, 0.0, 0.0 ];
      // Positon of first line vertex   
      vertices.push(...position1);    
      // Color of second line vertex 
      vertices.push(...COLOR_GREEN);   

      // Position of second vertex  
      let position2 = [j * delta, ymax, 0.0];
      // Positon of first line vertex   
      vertices.push(...position2);    
      // Color of second line vertex 
      vertices.push(...COLOR_GREEN);   
    }

    let buffer = new Vbuffer(gl, attr_position, attr_color);
    buffer.setData(vertices);
    return buffer;
}

function buildTetrahedron(gl, attr_position, attr_color, size)
{
  let k = size;
  // Position of Tetrahedron vertices 
  let a = [0, 0, k];  // Point in Z axis 
  let b = [k, 0, 0];  // Point in X axis 
  let c = [0, k, 0];  // Point in Y axis 
  let d = [0, 0, 0];  // Point in origin 

  let COLOR_Triangle1 = COLOR_BLUE;
  let COLOR_Triangle2 = COLOR_GREEN;
  let COLOR_Triangle3 = COLOR_RED;
  let COLOR_Triangle4 = COLOR_YELLLOW;

  let vertices = [];

  // triangle vertices of face 1
  vertices.push(...a, ...COLOR_Triangle1);
  vertices.push(...b, ...COLOR_Triangle1);
  vertices.push(...c, ...COLOR_Triangle1);

  // triangle vertices of face 2
  vertices.push(...a, ...COLOR_Triangle2);
  vertices.push(...c, ...COLOR_Triangle2);
  vertices.push(...d, ...COLOR_Triangle2);

  // triangle vertices of face 3
  vertices.push(...a, ...COLOR_Triangle3);
  vertices.push(...b, ...COLOR_Triangle3);
  vertices.push(...d, ...COLOR_Triangle3);

  // triangle vertices of face 4
  vertices.push(...d, ...COLOR_Triangle4);
  vertices.push(...c, ...COLOR_Triangle4);
  vertices.push(...b, ...COLOR_Triangle4);

  // console.log(" [TRACE] vertices = ", vertices);

  let buffer = new Vbuffer(gl, attr_position, attr_color);
  buffer.setData(vertices);
  return buffer;
}

// Note: OpenGL can only draw triangles and lines. 
// Therefore, the only way to draw a cube solid is to 
// draw the triangles for every face of the  cube.
// As a cube has 6 face and two triangles for every 
// face, it is necessary to draw 12 triangles. 
function buildCube(gl, attr_position, attr_color, size)
{
  let k = size; 
  // Cube vertices 
  //       x  y  z
  let a = [0, 0, k];
  let b = [0, k, k];
  let c = [k, k, k];
  let d = [k, 0, k];
  let e = [k, 0, 0];
  let f = [k, k, 0];
  let g = [0, k, 0];
  let h = [0, 0, 0];

  let vertices = [];

  function triangle(t1, t2, t3, color)
  {
    vertices.push(...t1, ...color);
    vertices.push(...t2, ...color);
    vertices.push(...t3, ...color);
  }
  // Face 1 of cube 
  triangle(a, b, c, COLOR_BLUE);
  triangle(a, c, d, COLOR_BLUE);
  // Face 2 of cube 
  triangle(c, d, e, COLOR_DARK_GREEN);
  triangle(c, f, e, COLOR_DARK_BLUE);
  // Face 3 of cube 
  triangle(a, d, e, COLOR_GREEN);
  triangle(a, h, e, COLOR_GREEN);
  // Face  4 of cube 
  triangle(a, h, g, COLOR_YELLLOW);
  triangle(a, b, g, COLOR_YELLLOW);
  // Face  5 of cube 
  triangle(b, c, g, COLOR_GRAY);
  triangle(c, g, f, COLOR_GRAY);
  // Face 6 of cube 
  triangle(g, h, f, COLOR_RED);
  triangle(h, f, e, COLOR_RED);

  console.log(" [TRACE] vertices = ", vertices);

  let buffer = new Vbuffer(gl, attr_position, attr_color);
  buffer.setData(vertices);
  return buffer;
}


// Vertices of (x, y) coordinates 
let vertices_triangle = [
   // --- Vertices ----- //  // (R, G, B) --- Colors -----//
    -0.25,  -0.25,  0,      1.0, 0.0, 0.0
  ,  0.00,  +0.25,  0,      0.0, 1.0, 0.0
  , +0.25,  -0.25,  0,      0.0, 0.0, 1.0  
]; 

// Upload vertices GPU 

let vboGrid = buildGridPlaneXY(gl, attr_position, attr_color, 20, 0.1);
let gridFactory = new EntityFactory(gl, vboGrid, u_model).setDrawLines();
let gridXY = gridFactory.create();
let gridXZ = gridFactory.create().rotate(90.0, [1.0, 0.0, 0.0]);


let vboTetrahedron = buildTetrahedron(gl, attr_position, attr_color, 0.50);
let TetrahedronFactory = new EntityFactory(gl, vboTetrahedron, u_model);
let tetrahedron1 = TetrahedronFactory.create();
tetrahedron1.translate(0.6, 0.25, 0.70).rotate(70, [5, 2, 3]);

let tetrahedron2 = TetrahedronFactory.create();
tetrahedron2.setDrawLineLoop();
tetrahedron2.translate(1.0, 1.25, 0.9).rotate(70, [1, 2, 2]);

let vboCube = buildCube(gl, attr_position, attr_color, 0.50);
let cubeFactory = new EntityFactory(gl, vboCube, u_model);
let cube1 = cubeFactory.create().translate(0.9, 0.60, 0.50).rotate(80, [1, 1, 1]);


let vboTriangle = new Vbuffer(gl, attr_position, attr_color);
vboTriangle.setData(vertices_triangle);
const triangleFactory = new EntityFactory(gl, vboTriangle, u_model);

let triangle1 = triangleFactory.create();
triangle1.draw();

let triangle2 = triangleFactory.create();
triangle2.setPosition(0.60, 0.50, 0.0);
triangle2.setScale(1.25);
triangle2.draw();


gl.enableVertexAttribArray(attr_position);
gl.enableVertexAttribArray(attr_color);


let camera = new Camera(gl, u_view, u_proj, u_camera_eye)
camera.setPosition( [2, 2, 2 ]).lookAt( [0, 0, 0] );



function drawScene()
{
    // resizeCanvasToDisplaySize(gl.canvas);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    // Screen background color is black 
    // => clearColor(Red intensity from 0 to 1, Green , Blue , Alpha)
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.clearDepth(1.0);         // Clear everything
    gl.enable(gl.DEPTH_TEST);   // Enable depth testing
    gl.depthFunc(gl.LEQUAL);    // Near things obscure far things
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    triangle1.draw();
    triangle2.draw();
    // triangleA.draw();
    // triangleB.draw();

    gridXY.draw(); 
    gridXZ.draw();

    tetrahedron1.draw();
    // tetrahedron2.draw();
    cube1.draw();

}

drawScene();

const KEY_ARROW_LEFT  = 37; 
const KEY_ARROW_RIGHT = 39;
const KEY_ARROW_DOWN  = 40;
const KEY_ARROW_UP    = 38;

function precisionRound(vector, digits)
{
  let factor = Math.pow(10, digits);
  return vector.map(x => Math.round(x * factor) / factor);
}

function keyboardListener(event)
{
    /** @param {number} key */ 
    let key = event.keyCode; 

    // console.log(" [TRACE] type(event) = ", typeof(event));
    // console.log(" [TRACE] Event = ", event);

    if( key == KEY_ARROW_LEFT )
    {
      // triangle1.translate(-0.10, 0.0, 0.0);
      camera.rotateYaw(10.0);
    } 

    if( key == KEY_ARROW_RIGHT  )
    {
      // triangle1.translate(+0.10, 0.0, 0.0);
      camera.rotateYaw(-10.0);
    } 

    if( key == KEY_ARROW_UP  )
    {
      // triangle1.translate(0.0, 0.1, 0.0);
      camera.moveForward(0.2);
    } 

    if( key == KEY_ARROW_DOWN  )
    {
      // triangle1.translate(0.0, -0.1, 0.0);
      camera.moveForward(-0.2);
    } 

    // Elevate camera view, rotate the forward vector upwards 
    if( event.key == "a" )
    {
      camera.rotatePitch(10.0);
    }

    // Rotate camera view downwards 
    if( event.key == "s")
    {
      camera.rotatePitch(-10.0);
    }

    if( event.key == "q")
    {
      cube1.rotate(25, [1, 2, 1]); 
    }

    if( event.key == "w")
    {
      console.log(" Rotate -10 degrees ");
      triangle1.rotateZ(-10.0);
    }

    if( event.key == "o")
    {
      // triangle1.setVisible(true);
      cube1.setDrawTriangles();
    }

    if(  event.key == "p" )
    {
      // triangle1.setVisible(false);
      cube1.setDrawLineLoop();
    }

    // Move camera in the positive direction of Y axis 
    if( event.key === "j" ){ camera.moveCameraUpDow(+0.1); }
    // Move camera in the negative direction of Y axis 
    if( event.key === "k" ){ camera.moveCameraUpDow(-0.1); }

    // Make the vector forward parallel to the plane XZ
    if( event.key == "h"){ camera.parallelXZ(); }

    let eye =  precisionRound(camera.eye, 3);
    let up =   precisionRound(camera.up, 3);
    let forward = precisionRound(camera.forward, 3);
    document.querySelector("#output-camera").textContent = `eye = ${eye} ; up = ${up} ; forward = ${forward}`;

    drawScene();
}

addEventListener("keydown", keyboardListener);

1.24 3D - Quaternion

The class Transform has the fields, position for setting the object position in world-coordinates; scale for adjusting object size and rotation, which is a quaternion for encoding rotation around an arbitrary axis. The use of quaternions saves the usage of many matrix multiplications for each transform and also allows easier accumulation of intermediate rotations.

Quaternions are also used in the camera class for providing YRP (Yaw-Pitch-Roll) camera rotation and free movement in the space. The keys arrow-left, arrow-right rotates the direction to where camera is looking at to right or to the left. The key arrow-up, moves the camera position in the direction to where the camera is looking at. The key arrow-down moves the camera in the backward direction to where the camera is looking at. The key 'S' rotates the camera up, the key 'D' rotates the camera down.

The scene, has a blue square on the plane XY; a green square on plane XZ; a teapot and torus. The torus can be moved, scaled and rotated in the space by just changing its transform object parameters. By typing the key 'B', the teapot scale is increased, and by typing 'N' its scale is decrease. The keys 'J', 'L' rotates the teapot around the Y axis. The key 'P' moves the teapot up across Y axis. The key 'O' moves the teapot down across the Y axis.

Note: This code requires the GLUT library which provides the sample wireframe teapot and torus.

Screenshots

opengl-draw3d-camera1.png

opengl-draw3d-camera2.png

Code Highlights

Transform class using quaternion:

// Transform object that combines - translation, scale and rotation (quaternion)
struct Transform
{
    glm::vec4 position = {0.0, 0.0, 0.0, 1.0};
    glm::vec3 scale    = {1.0, 1.0, 1.0};
    glm::quat rotation = {1.0, 0.0, 0.0, 0.0};

    void set_scale(float k){ scale[0]  = k; scale[1] = k;  scale[2]  = k;  }
    void add_scale(float k){ scale[0] += k; scale[1] += k; scale[2] += k;  }

    void set_position(float x, float y, float z)
    {  this->position = glm::vec4(x, y, z, 1.0);  };

    void set_rotation(float angle, glm::vec3 const& axis)
    { this->rotation = glm::angleAxis( glm::radians(angle), axis );  }

    void translate(float dx, float dy, float dz)
    {
        this->position[0] += dx;
        this->position[1] += dy;
        this->position[2] += dz;
    }

    // Rotation increment.
    void rotate(float angle, glm::vec3 const& axis)
    {
        auto q = glm::angleAxis( glm::radians(angle), axis );
        this->rotation = q * this->rotation;
    }

    // Get model affine transform - comprised of scale, translation and translation.
    glm::mat4 transform()
    {
        // Get transformation matrix from quaternion
        glm::mat4 trf = glm::mat4_cast(rotation);
        // Multiply all elements of column X axis scale
        trf[0] = scale[0] * glm::column(trf, 0);
        trf[1] = scale[1] * glm::column(trf, 1);
        trf[2] = scale[2] * glm::column(trf, 2);
        trf[3] = position;
        return trf;
    }
};

Camera class using quaternions for rotating the camera:

struct Camera
{
    // Current location of camera in world coordinates
    glm::vec3 eye  = { 2.0, 4.0, 5.0 };
    // Direction to where camera is looking at.
    glm::vec3 forward =  {-1.0, 0.0, -1.0 };
    // Current  camera up vector (orientation) - Y axis (default)
    glm::vec3 up = { 0.0, 1.0, 0.0 };

    // Field of view
    float fov_angle = glm::radians(60.0);
    // Aspect ratio
    float aspect   = 1.0;
    float zFar     = 20.0;
    float zNear    = 0.1;

    // ID of shader's view uniform variable - for setting view matrix
    GLuint shader_uniform_view;
    // ID of shader's projection uniform variable for setting projection matrix.
    GLuint shader_uniform_proj;

    Camera(GLuint uniform_view, GLuint uniform_proj, float aspect):
        shader_uniform_view(uniform_view), shader_uniform_proj(uniform_proj), aspect(aspect)
    {  update_view();  }

    void update_view()
    {
        // Point to where camera is looking at.
        auto cam_at = this->eye + this->forward;
        // Create View matrix (maps from world-space to camera-space)
        auto Tview = glm::lookAt(eye, cam_at, up);
        // Create projection matrix maps - camera-space to NDC (Normalized Device Coordinates).
        auto Tproj = glm::perspective( fov_angle, aspect, zNear, zFar );
        // Set shader uniform variables.
        glUniformMatrix4fv(shader_uniform_view, 1, GL_FALSE, glm::value_ptr(Tview) );
        glUniformMatrix4fv(shader_uniform_proj, 1, GL_FALSE, glm::value_ptr(Tproj) );
    }

    // Rotate around camera's Up vecto.
    void rotate_yaw(float angle)
    {
        // Build quaternion from axis angle
        glm::quat q = glm::angleAxis(glm::radians(angle), this->up);
        // Rotate current forward vector
        forward = forward * q;
        std::cout << " [FORWARD VECTOR ] " << glm::to_string(forward) << '\n';
        this->update_view();
    }

    // Rotate around camera's pitch axis (X axis) elevate camera view.
    void rotate_pitch(float angle)
    {
        glm::vec3 axis = glm::cross(this->up, this->forward);
        // Build quaternion from axis angle
        glm::quat q = glm::angleAxis(glm::radians(angle), axis);
        forward = q * forward;
        this->update_view();
    }

    void rotate_roll(float angle)
    {
        glm::quat q = glm::angleAxis(glm::radians(angle), this->forward);
        this->up = q * this->up;
        this->update_view();
    }

    // Move at forward vector direction (to where camera is looking at).
    void move_forward(float factor)
    {
        this->eye = this->eye + factor * this->forward;
        this->update_view();
    }
    // Move camera to specific point in the space.
    void set_position(float dx, float dy, float dz)
    {
        this->eye = glm::vec3(dx, dy, dz);
        this->update_view();
    }

    // Set position to where camera is looking at.
    void look_at(const glm::vec3& at)
    {
        this->forward = at - this->eye;
        this->update_view();
    }

};

Files

Vertex Shader Code:

#version 330 core

layout ( location = 0)  in vec3 position;
// layout ( location = 1) in vec3 color;
out vec3 out_color;
uniform mat4 u_model;       // Model matrix
uniform mat4 u_view;        // Camera's view matrix
uniform mat4 u_projection;  // Camera's projection matrix
uniform vec3 u_color;       // Unique color to all vertices set by the C++-side

void main()
{
    gl_Position = u_projection * u_view * u_model * vec4(position, 1.0);
    // Forward to fragment shader
    out_color = u_color;  // color; // vec3(0.56, 0.6, 0.0);
}

Fragment Shader Code:

#version 330

in vec3 out_color;

void main()
{
    // Set vertex colors
    gl_FragColor =  vec4(out_color, 1.0);
}

File: CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(GLFW_project)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)

# Set GLFW Options before FectchContent_MakeAvailable
set( GLFW_BUILD_EXAMPLES OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_TESTS    OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_DOCS     OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_INSTALL  OFF CACHE BOOL  "GLFW lib only" )

# Donwload GLFW library
FetchContent_Declare(
  glfwlib
  URL   https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
)
FetchContent_MakeAvailable(glfwlib)

# Download GLM (OpenGL math library for matrix and vectors transformation)
FetchContent_Declare(
  glm
  URL  https://github.com/g-truc/glm/archive/0.9.8.zip
)
FetchContent_MakeAvailable(glm)

message( [DEBUG] " glm_SOURCE_DIR = ${glm_SOURCE_DIR} ")
include_directories(${glm_SOURCE_DIR})

  # ======= TARGETS ===========================#

       add_executable( draw3d-camera  draw3d-camera.cpp        )
target_link_libraries( draw3d-camera  glfw OpenGL::GL GLU glut )

File: draw3d-camera.cpp

#include <iostream>
#include <vector>
#include <array>
#include <cmath>
#include <functional>
#include <iomanip>

// -------- OpenGL headers ---------//
//
#define GL_GLEXT_PROTOTYPES 1 // Necessary for OpenGL >= 3.0 functions
#define GL3_PROTOTYPES      1 // Necessary for OpenGL >= 3.0 functions

#include <GL/gl.h>
#include <GLFW/glfw3.h>

// #include <GL/glew.h>
#include <GL/glu.h>

// --------- OpenGL Math Librar ------------//
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/string_cast.hpp>
#include <glm/gtc/matrix_access.hpp>

#define GLUT_ENABLED

#if defined(GLUT_ENABLED)
    #include <GL/glut.h>
#endif

GLFWwindow* make_glfwWindowi(int  width, int height, const char* title);
// Compile some  shader
void compile_shader(GLuint m_program, const char* code, GLenum type);

// ------------ Basic Data Structures -----------//

// Wrapper for 2D vertex coordinates
struct Vertex3D{  GLfloat x, y, z;  };
// Wrapper for RGB colors
struct ColorRGB { GLfloat r, g, b; };

// Transform object that combines - translation, scale and rotation (quaternion)
struct Transform
{
    glm::vec4 position = {0.0, 0.0, 0.0, 1.0};
    glm::vec3 scale    = {1.0, 1.0, 1.0};
    glm::quat rotation = {1.0, 0.0, 0.0, 0.0};

    void set_scale(float k){ scale[0]  = k; scale[1] = k;  scale[2]  = k;  }
    void add_scale(float k){ scale[0] += k; scale[1] += k; scale[2] += k;  }

    void set_position(float x, float y, float z)
    {  this->position = glm::vec4(x, y, z, 1.0);  };

    void set_rotation(float angle, glm::vec3 const& axis)
    { this->rotation = glm::angleAxis( glm::radians(angle), axis );  }

    void translate(float dx, float dy, float dz)
    {
        this->position[0] += dx;
        this->position[1] += dy;
        this->position[2] += dz;
    }

    // Rotation increment.
    void rotate(float angle, glm::vec3 const& axis)
    {
        auto q = glm::angleAxis( glm::radians(angle), axis );
        this->rotation = q * this->rotation;
    }

    // Get model affine transform - comprised of scale, translation and translation.
    glm::mat4 transform()
    {
        // Get transformation matrix from quaternion
        glm::mat4 trf = glm::mat4_cast(rotation);
        // Multiply all elements of column X axis scale
        trf[0] = scale[0] * glm::column(trf, 0);
        trf[1] = scale[1] * glm::column(trf, 1);
        trf[2] = scale[2] * glm::column(trf, 2);
        trf[3] = position;
        return trf;
    }
};

struct Camera
{
    // Current location of camera in world coordinates
    glm::vec3 eye  = { 2.0, 4.0, 5.0 };
    // Direction to where camera is looking at.
    glm::vec3 forward =  {-1.0, -1.0, -1.0 };
    // Current  camera up vector (orientation) - Y axis (default)
    glm::vec3 up = { 0.0, 1.0, 0.0 };

    // Field of view
    float fov_angle = glm::radians(60.0);
    // Aspect ratio
    float aspect   = 1.0;
    float zFar     = 20.0;
    float zNear    = 0.1;

    // ID of shader's view uniform variable - for setting view matrix
    GLuint shader_uniform_view;
    // ID of shader's projection uniform variable for setting projection matrix.
    GLuint shader_uniform_proj;

    Camera(GLuint uniform_view, GLuint uniform_proj, float aspect):
        shader_uniform_view(uniform_view), shader_uniform_proj(uniform_proj), aspect(aspect)
    {  update_view();  }

    void update_view()
    {
        // Point to where camera is looking at.
        auto cam_at = this->eye + this->forward;
        // Create View matrix (maps from world-space to camera-space)
        auto Tview = glm::lookAt(eye, cam_at, up);
        // Create projection matrix maps - camera-space to NDC (Normalized Device Coordinates).
        auto Tproj = glm::perspective( fov_angle, aspect, zNear, zFar );
        // Set shader uniform variables.
        glUniformMatrix4fv(shader_uniform_view, 1, GL_FALSE, glm::value_ptr(Tview) );
        glUniformMatrix4fv(shader_uniform_proj, 1, GL_FALSE, glm::value_ptr(Tproj) );
    }

    // Rotate around camera's Up vecto.
    void rotate_yaw(float angle)
    {
        // Build quaternion from axis angle
        glm::quat q = glm::angleAxis(glm::radians(angle), this->up);
        // Rotate current forward vector
        forward = forward * q;
        std::cout << " [FORWARD VECTOR ] " << glm::to_string(forward) << '\n';
        this->update_view();
    }

    // Rotate around camera's pitch axis (X axis) elevate camera view.
    void rotate_pitch(float angle)
    {
        glm::vec3 axis = glm::cross(this->up, this->forward);
        // Build quaternion from axis angle
        glm::quat q = glm::angleAxis(glm::radians(angle), axis);
        forward = q * forward;
        this->update_view();
    }

    void rotate_roll(float angle)
    {
        glm::quat q = glm::angleAxis(glm::radians(angle), this->forward);
        this->up = q * this->up;
        this->update_view();
    }

    // Move at forward vector direction (to where camera is looking at).
    void move_forward(float factor)
    {
        this->eye = this->eye + factor * this->forward;
        this->update_view();
    }
    // Move camera to specific point in the space.
    void set_position(float dx, float dy, float dz)
    {
        this->eye = glm::vec3(dx, dy, dz);
        this->update_view();
    }

    // Set position to where camera is looking at.
    void look_at(const glm::vec3& at)
    {
        this->forward = at - this->eye;
        this->update_view();
    }

};

void send_buffer( GLuint* pVao, GLuint* pVbo, GLsizei sizeBuffer
        , void* pBufffer, GLint   shader_attr, GLint size, GLenum type);


constexpr ColorRGB color_red   = {1.0, 0.0, 0.0};
constexpr ColorRGB color_green = {0.0, 1.0, 0.0};
constexpr ColorRGB color_blue  = {0.0, 0.0, 1.0};
constexpr ColorRGB color_yellow    = { 0.80,  1.000, 0.100 };
constexpr ColorRGB color_gray       = { 0.47,  0.390, 0.380 };
constexpr ColorRGB color_dark_green = { 0.027, 0.392, 0.050 };
constexpr ColorRGB color_dark_blue  = { 0.109, 0.066, 0.411 };
constexpr ColorRGB color_white      = { 1.0, 1.0, 1.0 };

const glm::vec3 AXIS_X = { 1.0, 0.0, 0.0 };
const glm::vec3 AXIS_Y = { 0.0, 1.0, 0.0 };
const glm::vec3 AXIS_Z = { 0.0, 0.0, 1.0 };

int main(int argc, char** argv)
{

    /* Initialize the library */
    if (!glfwInit()){ return -1; }

    #if defined(GLUT_ENABLED)
        glutInit(&argc, argv);
    #endif
                                              //
    GLFWwindow* window = make_glfwWindowi(640, 480, "OpenGL quaternion");
    // Note: The shader source code is at the end of file.
    extern const char* code_vertex_shader;
    extern const char* code_frag_shader;

    // ====== S H A D E R - C O M P I L A T I O N ====//
    //
    GLuint prog = glCreateProgram();
    // Compile shader code
    compile_shader(prog, code_vertex_shader, GL_VERTEX_SHADER  ) ;
    compile_shader(prog, code_frag_shader,   GL_FRAGMENT_SHADER );
    glUseProgram(prog);

    // --------- Shader Uniform Variables -------//
    const GLint u_proj  = glGetUniformLocation(prog, "u_projection");
    const GLint u_view  = glGetUniformLocation(prog, "u_view");
    const GLint u_model = glGetUniformLocation(prog, "u_model");
    const GLint u_color = glGetUniformLocation(prog, "u_color");
    assert( u_color >= 0 );
    // ------- Shader attribute locations -------//
    const GLint attr_position = glGetAttribLocation(prog, "position");
    const GLint attr_color    = glGetAttribLocation(prog, "color");


    // ============== Set Shader Uniform Variables =============//
    //                                                          //

    int width, height;
    glfwGetWindowSize(window, &width, &height);
    // Window aspect ratio
    float aspect = static_cast<float>(width) / height;
    // Identity matrix
    const auto identity = glm::mat4(1.0);
    // Set projection matrix uniform variable
    glUniformMatrix4fv(u_proj, 1, GL_FALSE, glm::value_ptr(identity) );

    // ====== U P L O A D - TO - G P U =========================//

    Camera camera(u_view, u_proj, width / height);

    // Plane XY grid (vertical) - contains axis X and Y
    Transform model_grid_xy;
    model_grid_xy.set_position(0.0, 0.0, -2.0);

    // Plane XZ grid (horizontal) - contains axis X and Z
    Transform model_grid_xz;
    model_grid_xz.set_rotation(90.0f, AXIS_X);

    Transform model_teapot;
    model_teapot.set_scale(0.4);
    model_teapot.set_position(0.0, 0.50, 0.2);
    model_teapot.set_rotation(-90.0, glm::vec3(1.0, 0.0, 0.0) );


    auto plane_vertices = std::vector<Vertex3D>{
          {0.0, 0.0, 0.0}, {4.0, 0.0, 0.0}
        , {4.0, 0.0, 4.0} ,{0.0, 0.0, 4.0}
    };

    GLuint vao_plane          = 0;
    GLuint vbo_plane_vertices = 0;
    send_buffer( &vao_plane, &vbo_plane_vertices, sizeof(Vertex3D) * plane_vertices.size()
                , plane_vertices.data(), attr_position, 3, GL_FLOAT );

    Transform model_plane_xz;
    model_plane_xz.set_position(-2, 0, -2);

    Transform model_plane_xy;
    model_plane_xy.set_position(-2, 0, -2);
    model_plane_xy.set_rotation(-90.0, AXIS_X);

    Transform model_torus;
    model_torus.set_position(-3, 2, 0);
    model_torus.rotate(90.0, AXIS_Y);

    //  ======= R E N D E R  - L O O P ============//
    //                                             //
    while ( !glfwWindowShouldClose(window) )
    {
        glClear(GL_COLOR_BUFFER_BIT  | GL_DEPTH_BUFFER_BIT);

        // ====== BEGIN RENDERING ============//

            // ---- Draw horizontal plane XZ (square) ------------//
            glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(model_plane_xz.transform()) );
            glUniform3fv(u_color, 1, &color_green.r );
            glBindVertexArray(vao_plane);
            glDrawArrays(GL_QUADS, 0, 4 );

            // ---- Draw vertical plane XY (square) ------------//
            glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(model_plane_xy.transform()) );
            glUniform3fv(u_color, 1, &color_blue.r );
            glBindVertexArray(vao_plane);
            glDrawArrays(GL_QUADS, 0, 4 );

            // ----- Draw teapot over plane -----------------------//
            glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(model_teapot.transform()) );
            glUniform3fv(u_color, 1, &color_red.r );
            glutWireTeapot(2.5);

            // ----- Draw Torus ------------------------------------//
            glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(model_torus.transform()) );
            glUniform3fv(u_color, 1, &color_dark_green.r );
            glutWireTorus(1.0f, 2.00f, 32, 32);

        // ====== END RENDERING ==============//

        /* Swap front and back buffers */
        glfwSwapBuffers(window);
        /* Poll for and process events */
        glfwWaitEvents();

        if( glfwGetKey(window, 'Q' ) == GLFW_PRESS )
        {
             std::cout << " [TRACE] User typed Q =>> Shutdown program. Ok. " << '\n';
             break;
        }

        // Rotate camera around its Z axis or forward vector
        if( glfwGetKey(window, 'T') == GLFW_PRESS ){ camera.rotate_roll(+5.0);  }
        if( glfwGetKey(window, 'Y') == GLFW_PRESS ){ camera.rotate_roll(-5.0); }

        // Rotate camera around its local X axis
        if( glfwGetKey(window, 'S') == GLFW_PRESS ){ camera.rotate_pitch(+5.0); }
        if( glfwGetKey(window, 'D') == GLFW_PRESS ){ camera.rotate_pitch(-5.0); }


        // Rotate camera.
        if( glfwGetKey(window, GLFW_KEY_UP ) == GLFW_PRESS   ){ camera.move_forward(+0.2);  }
        if( glfwGetKey(window, GLFW_KEY_DOWN ) == GLFW_PRESS ){ camera.move_forward(-0.2);  }
        if( glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS  ){ camera.rotate_yaw(+10.0);   }
        if( glfwGetKey(window, GLFW_KEY_LEFT ) == GLFW_PRESS  ){ camera.rotate_yaw(-10.0);   }

        // Reset camera - look at origin
        if( glfwGetKey(window, 'R' ) == GLFW_PRESS  ){
            camera.up = AXIS_Y;
            camera.set_position(4.0, 3.0, 2.5);
            camera.look_at( {0.0, 0.0, 0.0} );
        }

        // Rotate teapot
        if( glfwGetKey(window,  'H' ) == GLFW_PRESS   ){ model_teapot.rotate(+10.0, AXIS_Y) ;  }
        if( glfwGetKey(window,  'K' ) == GLFW_PRESS   ){ model_teapot.rotate(-10.0, AXIS_Y) ;  }
        // Move teapot up and down across Y axis
        if( glfwGetKey(window,  'P' ) == GLFW_PRESS   ){ model_teapot.translate(0.0, +0.1, 0.0) ;  }
        if( glfwGetKey(window,  'O' ) == GLFW_PRESS   ){ model_teapot.translate(0.0, -0.1, 0.0) ;  }

        // Decrease or increase Teapot size
        if( glfwGetKey(window,  'B' ) == GLFW_PRESS   ){ model_teapot.add_scale(+0.1) ;  }
        if( glfwGetKey(window,  'N' ) == GLFW_PRESS   ){ model_teapot.add_scale(-0.1) ;  }

    }

    glfwTerminate();
    return 0;

} // --- End of main() -----//

// ---------- S H A D E R - P R O G R A M S  -------------------------//
//

// Minimal vertex shader =>> Runs on the GPU and processes each vertex.
const char* code_vertex_shader = R"(
    #version 330 core

    layout ( location = 0)  in vec3 position;
    // layout ( location = 1) in vec3 color;
    out vec3 out_color;
    uniform mat4 u_model;       // Model matrix
    uniform mat4 u_view;        // Camera's view matrix
    uniform mat4 u_projection;  // Camera's projection matrix
    uniform vec3 u_color;       // Unique color to all vertices set by the C++-side

    void main()
    {
        gl_Position = u_projection * u_view * u_model * vec4(position, 1.0);
        // Forward to fragment shader
        out_color = u_color;  // color; // vec3(0.56, 0.6, 0.0);
    }

)";

// Fragment shader source code
const char* code_frag_shader = R"(
    #version 330

    in vec3 out_color;

    void main()
    {
        // Set vertex colors
        gl_FragColor =  vec4(out_color, 1.0);
    }
)";

    // ====== I M P L E M E N T A T I O N S ==========//

GLFWwindow*
make_glfwWindowi(int  width, int height, const char* title)
{

    glfwSetErrorCallback([](int error, const char* description)
                         { std::fprintf( stderr, " [GLFW ERROR] Error = %d ; Description = %s \n"
                                        , error, description);
                         });

    GLFWwindow* window = glfwCreateWindow(width, height, title, NULL, NULL);
    assert( window != nullptr && "Failed  to create Window");

    // OpenGL context
    glfwMakeContextCurrent(window);
    // Pain whole screen as black - dark screen colors are better
    // for avoding eye strain due long hours staring on monitor.
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    // Set - OpenGL Core Profile - version 3.3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    glEnable(GL_COLOR_MATERIAL);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);

    return window;
}

void compile_shader(GLuint m_program, const char* code, GLenum type)
{
    GLint shader_id = glCreateShader( type );
    glShaderSource(shader_id, 1, &code, nullptr);
    glCompileShader(shader_id);

    GLint is_compiled = GL_FALSE;
    // Check shader compilation result
    glGetShaderiv(shader_id, GL_COMPILE_STATUS, &is_compiled);

    // If there is any shader compilation result,
    // print the error message.
    if( is_compiled == GL_FALSE)
    {
        std::cerr << " [ERROR] Shader compilation error. " << '\n';
        std::abort();
    }

    glAttachShader(m_program, shader_id);
    glDeleteShader(shader_id);
    glLinkProgram(m_program);
    GLint link_status = GL_FALSE;
    glGetProgramiv(m_program, GL_LINK_STATUS, &link_status);
    assert( link_status != GL_FALSE );
    // glUseProgram(m_program);
}

// Upload buffer from main memory to GPU VBO
// =>> Parameters VAO, VBO are allocated by the caller.
void send_buffer( GLuint* pVao, GLuint* pVbo, GLsizei sizeBuffer
                , void* pBufffer, GLint   shader_attr, GLint size
                , GLenum type)
{
    assert(pVao != nullptr);
    assert(pVbo != nullptr);
    GLuint& vao = *pVao;
    GLuint& vbo = *pVbo;
    // Generate and bind current VAO (Vertex Array Object)
    if(vao == 0){ glGenVertexArrays(1, &vao); }
    glBindVertexArray(vao);
    // Generate and bind current VBO (Vertex Buffer Object)
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Upload data to current VBO buffer in GPU
    glBufferData(GL_ARRAY_BUFFER, sizeBuffer, pBufffer, GL_STATIC_DRAW);
    glEnableVertexAttribArray(shader_attr);
    // Set data layout - how data will be interpreted.
    glVertexAttribPointer(shader_attr, size, type, GL_FALSE, 0, nullptr);
    // ------ Disable Global state set by this function -----//
    // Unbind VAO
    glBindVertexArray(0);
    // Unbind VBO
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Disable current shader attribute
    glDisableVertexAttribArray(shader_attr);

}

1.25 3D - Surface wireframe chart

This code draws a wireframe surface of a function of two variables y = f(x, z) using GL_LINE primitives.

\begin{equation} y = f(x, z) = \frac{10 \cdot \sin x \cdot \sin z}{z} \end{equation}

Screenshot

opengl-draw3d-surface-wireframe-plot.png

Figure 22: 3D surface wireframe chart of f(x, z)

Code Highlights

Data structures:

struct Position { GLfloat x, y, z;  };
struct Normal   { GLfloat x, y, z;  };
struct Color    { GLfloat r, g, b;  };

struct Vertex   {  
    Position position; // Vertex position in space 
    Normal   normal;   // Vertex normal vector for illumination (Future use)
    Color    color;    // Vertex color (RGB)
};


// Mesh - set of vertices (position and attributes) that represents 
// some surface or solid. 
struct Mesh 
{
    std::vector<Vertex> vertices{};
    std::vector<GLuint> indices{}; 
    GLfloat line_width = 1.0;
    GLenum  draw_type; 
    GLuint vao = 0, vbo = 0, ibo = 0;

    // Draw mesh using indices 
    void draw_indices()
    {
        // Check wheter the mesh was sent to GPU.
        assert( vao != 0 && ibo != 0 );
        // Note: The subroutine glLineWidth is stateful!!
        glLineWidth(line_width);
        glBindVertexArray(vao);
        glDrawElements(draw_type, indices.size(), GL_UNSIGNED_INT, nullptr);
    }

    // Draw mesh using vertices 
    void draw_vertices()
    {
        assert( vao != 0 );
        glLineWidth(line_width);       
        glBindVertexArray(vao);
        glDrawArrays(draw_type, 0, vertices.size());
    }
};

Subroutine for generating surface mesh:

Mesh generate_surface_wireframe_mesh(  
                             float xmin, float xmax
                           , float zmin, float zmax
                           , size_t Nx, size_t Nz
                           , Color color 
                           , SurfaceFunction const& function)
{
    assert( xmax > xmin );
    assert( zmax > zmin );
    assert( Nx  != 0 && Nz  != 0 );

    std::cout << " [TRACE] Nx = " << Nx << " ; Nz = " << Nz << '\n';

    Mesh mesh;
    mesh.draw_type = GL_LINES;

    float dx = (xmax - xmin) / Nx;
    float dz = (zmax - zmin) / Nz; 

    // Intial allocation for avoiding many dynamic allocations
    mesh.vertices.reserve( Nx * Nz );
    mesh.indices.reserve( 2 * Nx * Nz  );
    float x, y, z, y_h;

    // Draw lines parallel to plane XY  
    for(size_t j = 0; j < Nz; j++)
    {   for(size_t i = 0; i < Nx; i++)
        {
            x = xmin + i * dx;
            z = zmin + j * dz; 
            y = function(x, z);
            y_h = function(x + dx, z);

            // Draw line segments in planes parallel to plane XY.
            //---------------------------------------------------------------------//
            // Initial vertex of line  segment
            mesh.vertices.push_back( Vertex{ Position{x, y, z}, zero_normal, color } );        
            // Final vertex of line segment 
            mesh.vertices.push_back( Vertex{ Position{x + dx, y_h, z}, zero_normal, color } );

        }
    }

    for(size_t i = 0; i < Nx; i++)
    {   for(size_t j = 0; j < Nz; j++)
        {
            x = xmin + i * dx;
            z = zmin + j * dz; 
            y = function(x, z);
            y_h = function(x, z + dz);

            // Draw line segments in planes parallel to plane YZ.
            mesh.vertices.push_back( Vertex{ Position{x, y, z       }, zero_normal, color } );
            mesh.vertices.push_back( Vertex{ Position{x, y_h, z + dz}, zero_normal, color } );

         }
    }
    return mesh;
}

Vertex shader:

#version 330 core 

layout ( location = 0)  in vec3 position;
layout ( location = 1) in vec3 color;   

out vec3 out_color;
out vec3 out_coord; 
out vec3 out_eye;

uniform vec3 u_eye;  // Cameras's position in space
uniform mat4 u_model;       // Model matrix 
uniform mat4 u_view;        // Camera's view matrix    
uniform mat4 u_projection;  // Camera's projection matrix 


void main()
{
    gl_Position = u_projection * u_view * u_model * vec4(position, 1.0);

    // Forward to fragment shader 
    out_coord  = position;
    out_color = color; // vec3(0.56, 0.6, 0.0);
    out_eye = u_eye;
}

Fragment shader:

#version 330 

in vec3 out_color;
in vec3 out_coord;
in vec3 out_eye; 

void main()
{
    float k;

    if(gl_FrontFacing)
        k = 1.0; // 2.0;
    else 
        k = 0.1; // 0.01;

    float factor = distance(out_eye, out_coord) ;

    // Set vertex colors
    vec3 color = (out_coord / 2.0 / factor ) + k * out_color * ( 20.0 / factor ) ;
    // vec3 color =  k * out_color * ( 20.0 / factor ) ;

    // vec3 color = 1.0 / fac + (out_color / 2.0 + 0.5) * k;

    gl_FragColor =  vec4( color , 1.0);
    // gl_FragColor = vec4(0.3, 0.6, 0.0, 1.0);
}

Source code

File: CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(OpenGL_3D_Wireframe_Surface_Chart)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)

# Set GLFW Options before FectchContent_MakeAvailable
set( GLFW_BUILD_EXAMPLES OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_TESTS    OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_DOCS     OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_INSTALL  OFF CACHE BOOL  "GLFW lib only" )

# Donwload GLFW library
FetchContent_Declare(
  glfwlib
  URL   https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
)
FetchContent_MakeAvailable(glfwlib)

# Download GLM (OpenGL math library for matrix and vectors transformation)
FetchContent_Declare(
  glm
  URL  https://github.com/g-truc/glm/archive/0.9.8.zip
)
FetchContent_MakeAvailable(glm)
include_directories(${glm_SOURCE_DIR})


# Download pre-compiled GLEW when building under Windows NT OS (x64)
IF(WIN32)
   FetchContent_Declare(
      glew-release 
      URL    https://ufpr.dl.sourceforge.net/project/glew/glew/2.1.0/glew-2.1.0-win32.zip 
      # https://github.com/nigels-com/glew/archive/glew-2.2.0.zip
   )
   FetchContent_MakeAvailable(glew-release)
   include_directories( ${glew-release_SOURCE_DIR}/include  ${glm_SOURCE_DIR} )        
   link_directories(  ${glew-release_SOURCE_DIR}/lib/Release/x64 )

   set( GLEW_LIB_PATH1 ${glew-release_SOURCE_DIR}/lib/Release/x64/glew32.lib )
   set( GLEW_LIB_PATH2 ${glew-release_SOURCE_DIR}/lib/Release/x64/glew32s.lib )
ENDIF()


MACRO(ADD_OPENGL_APP target sources)
   add_executable( ${target} ${sources} )
   message([TRACE] " Add OpenGL executable = ${target} ")

   target_link_libraries( ${target} glfw
                                    OpenGL::GL
                                    ${GLEW_LIB_PATH1}
                                    ${GLEW_LIB_PATH2} )

    IF(MINGW)
        # Statically link against MINGW dependencies
        # for making easier to deploy on other machines. 
        target_link_options( ${target} PRIVATE                                 
                                 -static-libgcc
                                 -static-libstdc++
                                 -Wl,-Bstatic,--whole-archive -lwinpthread
                                   -Wl,--no-whole-archive                                    
                                 )
     ENDIF()       


     # Copy GLEW DLL shared library to same directory as the executable.                 
     IF(WIN32)                           
        add_custom_command(TARGET ${target} POST_BUILD 
                       COMMAND ${CMAKE_COMMAND} -E copy_if_different
                       "${glew-release_SOURCE_DIR}/bin/Release/x64/glew32.dll"              
                       $<TARGET_FILE_DIR:${target}>)
     ENDIF()                                       
ENDMACRO()     

     # ======= T A R G E T S ============================#
     #                                                   #

ADD_OPENGL_APP( draw3d-plot-surface draw3d-plot-surface.cpp )

File: draw3d-plot-surface.cpp

#include <iostream>
#include <string> 
#include <sstream>
#include <vector> 
#include <array>
#include <cmath>
#include <functional>
#include <thread>

// -------- OpenGL headers ---------//
//
#define GL_GLEXT_PROTOTYPES 1 // Necessary for OpenGL >= 3.0 functions
#define GL3_PROTOTYPES      1 // Necessary for OpenGL >= 3.0 functions

#include <GL/gl.h>
#include <GLFW/glfw3.h>

// #include <GL/glew.h>
#include <GL/glu.h>

// --------- OpenGL Math Librar ------------//
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/string_cast.hpp>

// #define GLUT_ENABLED 

#if defined(GLUT_ENABLED)
    #include <GL/glut.h>
#endif 

#define GL_CHECK(funcall)\
    do { \
        (funcall); \
        GLint error = glGetError();  \
        if(error == GL_NO_ERROR){ break; } \
        std::fprintf(stderr, " [OPENGL ERROR] Error code = %d ; line = %d ; call = '%s'  \n" \
                      , error, __LINE__, #funcall ); \
        abort(); \
    } while(0)

GLFWwindow* make_glfwWindow(int  width, int height, const char* title);

// Compile some  shader 
void compile_shader(GLuint m_program, const char* code, GLenum type);

// ------------ Basic Data Structures -----------// 
struct Position { GLfloat x, y, z;  };
struct Normal   { GLfloat x, y, z;  };
struct Color    { GLfloat r, g, b;  };

struct Vertex   {  
    Position position; // Vertex position in space 
    Normal   normal;   // Vertex normal vector for illumination (Future use)
    Color    color;    // Vertex color (RGB)
};

std::ostream& operator<<(std::ostream& os, Vertex const& v)
{
    return os << " Vertex{ x = " << v.position.x << " ; " 
              << " y = " << v.position.y << " ; z = " << v.position.z << " } ";
}

// Mesh - set of vertices (position and attributes) that represents 
// some surface or solid. 
struct Mesh 
{
    std::vector<Vertex> vertices{};
    std::vector<GLuint> indices{}; 
    GLfloat line_width = 1.0;
    GLenum  draw_type; 
    GLuint vao = 0, vbo = 0, ibo = 0;

    // Draw mesh using indices 
    void draw_indices()
    {
        // Check wheter the mesh was sent to GPU.
        assert( vao != 0 && ibo != 0 );
        // Note: The subroutine glLineWidth is stateful!!
        glLineWidth(line_width);
        glBindVertexArray(vao);
        glDrawElements(draw_type, indices.size(), GL_UNSIGNED_INT, nullptr);
    }

    // Draw mesh using vertices 
    void draw_vertices()
    {
        assert( vao != 0 );
        glLineWidth(line_width);       
        glBindVertexArray(vao);
        glDrawArrays(draw_type, 0, vertices.size());
    }
};


// Send/upload mesh data to GPU 
// attr_position =>> Shader attribute location for vertex position 
// attr_color    =>> Shader attribute location for vertex color 
void send_mesh( Mesh& mesh , GLint attr_position , GLint attr_color ); 

constexpr Position origin = { 0.0, 0.0, 0.0};
constexpr Position position_x(float x){ return {x, 0.0, 0.0};  }
constexpr Position position_y(float y){ return {0.0, y, 0.0};  }
constexpr Position position_z(float z){ return {0.0, 0.0, z};  }

constexpr Normal zero_normal = { 0.0, 0.0 , 0.0};
constexpr Color  color_red   = {1.0, 0.0, 0.0};
constexpr Color  color_green = {0.0, 1.0, 0.0};        
constexpr Color  color_blue  = {0.0, 0.0, 1.0};

const glm::mat4 matrix_identity = glm::mat4(1.0);


struct Camera
{
    // Current location of camera in world coordinates
    glm::vec3 eye  = { 2.0, 4.0, 5.0 };
    // Direction to where camera is looking at.
    glm::vec3 forward =  {-1.0, 0.0, -1.0 };
    // Current  camera up vector (orientation) - Y axis (default)
    glm::vec3 up = { 0.0, 1.0, 0.0 };

    // Field of view
    float fov_angle = glm::radians(60.0);
    // Aspect ratio
    float aspect   = 1.0;
    float zFar     = 100.0;
    float zNear    = 0.1;

    // ID of shader's view uniform variable - for setting view matrix
    GLint shader_uniform_view = -1;
    // ID of shader's projection uniform variable for setting projection matrix.
    GLint shader_uniform_proj = -1;

    GLint shader_uniform_eye = -1;

    Camera(GLuint uniform_view, GLuint uniform_proj, GLuint uniform_eye, float aspect):
         shader_uniform_view(uniform_view)
       , shader_uniform_proj(uniform_proj)
       , shader_uniform_eye(uniform_eye)
       , aspect(aspect)
    {  update_view();  }

    void update_view()
    {

        // Point to where camera is looking at.
        auto cam_at = this->eye + this->forward;
        // Create View matrix (maps from world-space to camera-space)
        auto Tview = glm::lookAt(eye, cam_at, up);
        // Create projection matrix maps - camera-space to NDC (Normalized Device Coordinates).
        auto Tproj = glm::perspective( fov_angle, aspect, zNear, zFar );
        // Set shader uniform variables.
        glUniformMatrix4fv(shader_uniform_view, 1, GL_FALSE, glm::value_ptr(Tview) );
        glUniformMatrix4fv(shader_uniform_proj, 1, GL_FALSE, glm::value_ptr(Tproj) );
        glUniform3fv(shader_uniform_eye, 1, glm::value_ptr(eye));
    }

    // Rotate around camera's Up vecto.
    void rotate_yaw(float angle)
    {
        // Build quaternion from axis angle
        glm::quat q = glm::angleAxis(glm::radians(angle), this->up);
        // Rotate current forward vector
        forward = forward * q;
        // std::cout << " [FORWARD VECTOR ] " << glm::to_string(forward) << '\n';
        this->update_view();
    }

    // Rotate around camera's pitch axis (X axis) elevate camera view.
    void rotate_pitch(float angle)
    {
        glm::vec3 axis = glm::cross(this->up, this->forward);
        // Build quaternion from axis angle
        glm::quat q = glm::angleAxis(glm::radians(angle), axis);
        forward = q * forward;
        this->update_view();
    }

      // Move at forward vector direction (to where camera is looking at).
    void move_forward(float factor)
    {
        this->eye = this->eye + factor * this->forward;
        this->update_view();
    }
    // Move camera to specific point in the space.
    void set_position(float x, float y, float z)
    {
        this->eye = glm::vec3(x, y, z);
        this->update_view();
    }

    // Set position to where camera is looking at.
    void look_at(const glm::vec3& at)
    {
        this->forward = at - this->eye;
        this->update_view();
    }

};

using SurfaceFunction = std::function<float (float, float)>;

// Nx - Number of points in the Xmin, Xmax interval 
// Ny - Number of points in the Ymin, Ymax interval 
Mesh generate_surface_wireframe_mesh(  
                             float xmin, float xmax
                           , float zmin, float zmax
                           , size_t Nx, size_t Nz
                           , Color color 
                           , SurfaceFunction const& function);

int main(int argc, char** argv)
{

    /* Initialize the library */
    if (!glfwInit()){ return -1; }

    #if defined(GLUT_ENABLED)
        glutInit(&argc, argv);
    #endif

    // ====== S H A D E R - C O M P I L A T I O N ====//
    //                                                // 

    GLFWwindow* window = make_glfwWindow(640, 480, "Plot 3D surface");

    // Note: The shader source code is at the end of file.
    extern const char* code_vertex_shader;
    extern const char* code_frag_shader;

    GLuint prog = glCreateProgram();
    // Compile shader code 
    compile_shader(prog, code_vertex_shader, GL_VERTEX_SHADER  ) ;    
    compile_shader(prog, code_frag_shader,   GL_FRAGMENT_SHADER );
    glUseProgram(prog);

    // Get shader uniform variable location for projection matrix
    // See shader code: "uniform mat4 projection;"
    const GLint u_proj  = glGetUniformLocation(prog, "u_projection");
    assert( u_proj >= 0 && "Failed to find u_projection uniform variable" );

    const GLint u_view = glGetUniformLocation(prog, "u_view");
    assert( u_proj >= 0 && "Failed to find u_view uniform variable" );

    // Get shader uniform variable  location for model matrix.
    const GLint u_model  = glGetUniformLocation(prog, "u_model");
    assert( u_model >= 0 && "Failed to find uniform variable" );  

    const GLint u_camera_eye = glGetUniformLocation(prog, "u_eye");
    printf(" [TRACE] u_camera_eye = %d \n", u_camera_eye);
    assert( u_camera_eye >= 0 );  

    // Get shader attribute location - the function glGetAttribLocation - returns (-1) on error.
    const GLint attr_position = glGetAttribLocation(prog, "position");
    assert( attr_position >= 0 && "Failed to get attribute location" );

    // Get shader attribute of color 
    const GLint attr_color = glGetAttribLocation(prog, "color");
    if( attr_color < 0){ std::fprintf(stderr, " [WARNING] Shader color attribute location not found. \n"); };

    glUniformMatrix4fv(u_model, 1, GL_FALSE, glm::value_ptr(matrix_identity) );
    glUniformMatrix4fv(u_proj,  1, GL_FALSE, glm::value_ptr(matrix_identity) );

    // ====== U P L O A D - TO - G P U =========================// 
    //                                                          //

    // ----- X, Y, Z axis for visual debugging ----------------
    // X axis (GREEN) ; Y axis (RED); Z axis (BLUE)
    GLuint vao_axis = 0; 
    GLuint vbo_axis_vertices = 0;
    GLuint vbo_axis_colors = 0;
    float axis_len = 100.0;


    Mesh mesh_axis{};
    mesh_axis.vertices = {
          // X axis line  
          { origin,                  zero_normal,  color_green }
        , { position_x(axis_len),    zero_normal,  color_green }
          // Y axis line 
        , { origin,                   zero_normal,  color_red  }
        , { position_y(axis_len),     zero_normal,  color_red  }  
          //  Z axis line
        , { origin,                   zero_normal,  color_blue }
        , { position_z(axis_len),     zero_normal,  color_blue } 
    };
    mesh_axis.line_width = 10.0;
    mesh_axis.draw_type  = GL_LINES;
    send_mesh(mesh_axis, attr_position, attr_color);


    Mesh mesh_surface = generate_surface_wireframe_mesh(
                              -20, +20.0  // Xmax, Xmin 
                            , -20, +20.0  // Zmax, Zmin 
                            ,  200, 200   // Nx, Nz     => Number of points on X and Z axis
                            , color_green // Surface wireframe color 
                            // , [&](float x, float z){ return 25.0 - x * x - z * z ;}
                            , [&](float x, float z){ return 10.0 * sin(x) * sin(z) / z  ; }
                        ); 
    mesh_surface.line_width = 0.25;
    mesh_surface.draw_type  = GL_LINES;
    send_mesh(mesh_surface, attr_position, attr_color);

    // ============== Set Shader Uniform Variables =============// 
    //                                                          // 

    int width, height;
    glfwGetWindowSize(window, &width, &height);
    // Window aspect ratio
    float aspect = static_cast<float>(width) / height;

    // Set projection matrix uniform variable
    // glUniformMatrix4fv(u_proj, 1, GL_FALSE, glm::value_ptr(identity) );

    Camera camera(u_view, u_proj, u_camera_eye, width / height);
    camera.set_position(-23.0, 21.0, -22.0);
    camera.look_at({0.0, 0.0, 0.0});

    //  ======= R E N D E R  - L O O P ============//
    //                                             //
    while ( !glfwWindowShouldClose(window) )
    {
        glClear(GL_COLOR_BUFFER_BIT  | GL_DEPTH_BUFFER_BIT);

        // ====== BEGIN RENDERING ============//    

            // Draw axis lines
            mesh_axis.draw_vertices();           

            // Draw surface
            mesh_surface.draw_vertices(); 
            // mesh_surface.draw_indices();


        // ====== END RENDERING ==============//

        /* Swap front and back buffers */
        glfwSwapBuffers(window);
        /* Poll for and process events */
        glfwWaitEvents();

        if( glfwGetKey(window, 'Q' ) == GLFW_PRESS )
        {
             std::cout << " [TRACE] User typed Q =>> Shutdown program. Ok. " << '\n';
             break;
        }

        // Move camera to direction to where it is looking at.
        if( glfwGetKey(window, GLFW_KEY_UP ) == GLFW_PRESS    ){ camera.move_forward(+0.5);  }
        if( glfwGetKey(window, GLFW_KEY_DOWN ) == GLFW_PRESS  ){ camera.move_forward(-0.5);  }

        // Rotate around its Y axis 
        if( glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS  ){ camera.rotate_yaw(+5.0);   }
        if( glfwGetKey(window, GLFW_KEY_LEFT ) == GLFW_PRESS  ){ camera.rotate_yaw(-5.0);   }

        // Rotate camera around its local X axis
        if( glfwGetKey(window, 'S') == GLFW_PRESS ){ camera.rotate_pitch(+5.0); }
        if( glfwGetKey(window, 'D') == GLFW_PRESS ){ camera.rotate_pitch(-5.0); }

        // Reset camera - look at origin
        if( glfwGetKey(window, 'R' ) == GLFW_PRESS  )
        {
            camera.set_position(4.0, 3.0, 2.5);
            camera.look_at( {0.0, 0.0, 0.0} );
        }

    }

    glfwTerminate();
    return 0;

} // --- End of main() -----//


// ---------- S H A D E R - P R O G R A M S  -------------------------//
//

// Minimal vertex shader =>> Runs on the GPU and processes each vertex.
const char* code_vertex_shader = R"(    
    #version 330 core 

    layout ( location = 0)  in vec3 position;
    layout ( location = 1) in vec3 color;   

    out vec3 out_color;
    out vec3 out_coord; 
    out vec3 out_eye;

    uniform vec3 u_eye;  // Cameras's position in space
    uniform mat4 u_model;       // Model matrix 
    uniform mat4 u_view;        // Camera's view matrix    
    uniform mat4 u_projection;  // Camera's projection matrix 


    void main()
    {
        gl_Position = u_projection * u_view * u_model * vec4(position, 1.0);

        // Forward to fragment shader 
        out_coord  = position;
        out_color = color; // vec3(0.56, 0.6, 0.0);
        out_eye = u_eye;
    }

)";

// Fragment shader source code 
const char* code_frag_shader = R"(
    #version 330 

    in vec3 out_color;
    in vec3 out_coord;
    in vec3 out_eye; 

    void main()
    {
        float k;

        if(gl_FrontFacing)
            k = 1.0; // 2.0;
        else 
            k = 0.1; // 0.01;

        float factor = distance(out_eye, out_coord) ;

        // Set vertex colors
        vec3 color = (out_coord / 2.0 / factor ) + k * out_color * ( 20.0 / factor ) ;
        // vec3 color =  k * out_color * ( 20.0 / factor ) ;

        // vec3 color = 1.0 / fac + (out_color / 2.0 + 0.5) * k;

        gl_FragColor =  vec4( color , 1.0);
        // gl_FragColor = vec4(0.3, 0.6, 0.0, 1.0);
    }
)";


    // ====== I M P L E M E N T A T I O N S ==========// 

GLFWwindow* 
make_glfwWindow(int  width, int height, const char* title)
{

    glfwSetErrorCallback([](int error, const char* description)
                         { std::fprintf( stderr, " [GLFW ERROR] Error = %d ; Description = %s \n"
                                        , error, description);
                         });

    GLFWwindow* window = glfwCreateWindow(width, height, title, NULL, NULL);
    assert( window != nullptr && "Failed  to create Window");

    // OpenGL context 
    glfwMakeContextCurrent(window);
    // Pain whole screen as black - dark screen colors are better 
    // for avoding eye strain due long hours staring on monitor.
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    // Set - OpenGL Core Profile - version 3.3
    GL_CHECK( glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3) ); 
    GL_CHECK( glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3) );
    GL_CHECK( glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE) );

    GL_CHECK( glEnable(GL_COLOR_MATERIAL) );
    GL_CHECK( glEnable(GL_DEPTH_TEST)     );
    GL_CHECK( glEnable(GL_BLEND)          );

    return window;
}

void compile_shader(GLuint m_program, const char* code, GLenum type)
{
    GLint shader_id = glCreateShader( type );
    glShaderSource(shader_id, 1, &code, nullptr);
    glCompileShader(shader_id);
    GLint is_compiled = GL_FALSE;
    // Check shader compilation result 
    glGetShaderiv(shader_id, GL_COMPILE_STATUS, &is_compiled);
    // If there is any shader compilation result, 
    // print the error message.
    if( is_compiled == GL_FALSE)
    {
        GLint length;
        glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &length);
        assert( length > 0 );

        std::string out(length + 1, 0x00);
        GLint chars_written;
        glGetShaderInfoLog(shader_id, length, &chars_written, out.data());
        std::cerr << " [SHADER ERROR] = " << out << '\n';
        // Abort the exection of current process. 
        std::abort();
    }          

    glAttachShader(m_program, shader_id);   
    glDeleteShader(shader_id);
    glLinkProgram(m_program);
    GLint link_status = GL_FALSE;
    glGetProgramiv(m_program, GL_LINK_STATUS, &link_status);
    assert( link_status != GL_FALSE );   
    // glUseProgram(m_program);         
}

//  Send  mesh data to GPU ----//
void send_mesh( Mesh& mesh , GLint attr_position , GLint attr_color   ) 
{ 
    GLuint& vao = mesh.vao;
    GLuint& vbo = mesh.vbo;
    GLuint& ibo = mesh.ibo;

    // ------------- Upload vertices --------------//

    // Generate and bind current VAO (Vertex Array Object)
    if(vao == 0){ glGenVertexArrays(1, &vao); }
    glBindVertexArray(vao);
    // Generate and bind current VBO (Vertex Buffer Object)
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    // Upload data to current VBO buffer in GPU 
    glBufferData(GL_ARRAY_BUFFER, mesh.vertices.size() * sizeof(Vertex)
                , mesh.vertices.data(), GL_STATIC_DRAW);   

    glEnableVertexAttribArray(attr_position);    
    glEnableVertexAttribArray(attr_color);    

    // Set data layout - how data will be interpreted.
    // => Each vertex has 2 coordinates. 
    glVertexAttribPointer( attr_position, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);

    glVertexAttribPointer( attr_color, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex)     
                           // Offset to color member variable in class Vertex2D
                          , reinterpret_cast<void*>( offsetof(Vertex, color) )
                          );

    // -------- Upload indices ----------------------//
    //
    if (!mesh.indices.empty())
    {
        glBindVertexArray(vao);
        glGenBuffers(1, &ibo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, mesh.indices.size() * sizeof(GLuint)
                     , mesh.indices.data(), GL_STATIC_DRAW );
    }

    // ------ Disable Global state set by this function -----//
    // Unbind VAO 
    glBindVertexArray(0);
    // Unbind VBO 
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // Unbind IBO 
    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0);
    // Disable current shader attribute 
    glDisableVertexAttribArray(attr_position);
    glDisableVertexAttribArray(attr_color);

}


Mesh generate_surface_wireframe_mesh(  
                             float xmin, float xmax
                           , float zmin, float zmax
                           , size_t Nx, size_t Nz
                           , Color color 
                           , SurfaceFunction const& function)
{
    assert( xmax > xmin );
    assert( zmax > zmin );
    assert( Nx  != 0 && Nz  != 0 );

    std::cout << " [TRACE] Nx = " << Nx << " ; Nz = " << Nz << '\n';

    Mesh mesh;
    mesh.draw_type = GL_LINES;

    float dx = (xmax - xmin) / Nx;
    float dz = (zmax - zmin) / Nz; 

    // Intial allocation for avoiding many dynamic allocations
    mesh.vertices.reserve( Nx * Nz );
    mesh.indices.reserve( 2 * Nx * Nz  );
    float x, y, z, y_h;

    // Draw lines parallel to plane XY  
    for(size_t j = 0; j < Nz; j++)
    {   for(size_t i = 0; i < Nx; i++)
        {
            x = xmin + i * dx;
            z = zmin + j * dz; 
            y = function(x, z);
            y_h = function(x + dx, z);

            // Draw line segments in planes parallel to plane XY.
            mesh.vertices.push_back( Vertex{ Position{x, y, z}, zero_normal, color } );
            mesh.vertices.push_back( Vertex{ Position{x + dx, y_h, z}, zero_normal, color } );

        }
    }

    for(size_t i = 0; i < Nx; i++)
    {   for(size_t j = 0; j < Nz; j++)
        {
            x = xmin + i * dx;
            z = zmin + j * dz; 
            y = function(x, z);
            y_h = function(x, z + dz);

            // Draw line segments in planes parallel to plane YZ.
            mesh.vertices.push_back( Vertex{ Position{x, y, z       }, zero_normal, color } );
            mesh.vertices.push_back( Vertex{ Position{x, y_h, z + dz}, zero_normal, color } );

         }
    }
    return mesh;
}

1.26 3D - 3D model loading with illumination

This sample code loads 3D meshes from obj file with help from tinyObjLoader library and draws the loaded meshes (3D models) triangles using ambient and diffuse light.

Libraries used:

  • glfw
    • Library for Window Abstraction and dealing with OpenGL context.
  • GLM math library
    • Computer graphics linear algebra library
  • Glew
    • Library for loading OpenGL function pointers. It is used only on Windows in Pre-compiled form due to the difficulty to build the library from source as it requires perl.
  • tinyobjloader
    • Library for loading 3D meshes from Obj files.
  • cmrc - Cmake Resource Compiler
    • Single-file CMake library for embedding files in the executable, shader scripts and 3D models. This Cmake utility makes easier to distribute the binary, since just a single file is distributed or deployed and the application does not have to deal with file system path and relative path.

See also:

Screenshot

opengl-draw3d-load-3dmodels.png

Files

File: CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project( OpenGL_3D_Model_Loader )

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

#================ GLFW Settings  ===============#

find_package(OpenGL REQUIRED)

include(FetchContent)

# Set GLFW Options before FectchContent_MakeAvailable 
set( GLFW_BUILD_EXAMPLES OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_TESTS    OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_DOCS     OFF CACHE BOOL  "GLFW lib only" )
set( GLFW_BUILD_INSTALL  OFF CACHE BOOL  "GLFW lib only" )

# Donwload GLFW library
FetchContent_Declare(
  glfwlib
  URL   https://github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
)
FetchContent_MakeAvailable(glfwlib)

# Download GLM (OpenGL math library for matrix and vectors transformation)
FetchContent_Declare(
  glm  
  URL  https://github.com/g-truc/glm/archive/0.9.8.zip
)
FetchContent_MakeAvailable(glm)
include_directories( ${glm_SOURCE_DIR} )

FetchContent_Declare(
  tinyobjloader 
  URL   https://github.com/tinyobjloader/tinyobjloader/archive/v2.0.0rc8.zip
)
FetchContent_MakeAvailable(tinyobjloader)


# Download pre-compiled GLEW when building under Windows NT OS (x64)
IF(WIN32)
   FetchContent_Declare(
      glew-release 
      URL   https://ufpr.dl.sourceforge.net/project/glew/glew/2.1.0/glew-2.1.0-win32.zip 
      # https://github.com/nigels-com/glew/archive/glew-2.2.0.zip
   )
   FetchContent_MakeAvailable(glew-release)
   include_directories( ${glew-release_SOURCE_DIR}/include  ${glm_SOURCE_DIR} )        
   link_directories(  ${glew-release_SOURCE_DIR}/lib/Release/x64 )

   set( GLEW_LIB_PATH1 ${glew-release_SOURCE_DIR}/lib/Release/x64/glew32.lib )
   set( GLEW_LIB_PATH2 ${glew-release_SOURCE_DIR}/lib/Release/x64/glew32s.lib )
ENDIF()

## Fetch Cmake-Resource Compiler
file(DOWNLOAD "https://raw.githubusercontent.com/vector-of-bool/cmrc/master/CMakeRC.cmake"
                 "${CMAKE_BINARY_DIR}/CMakeRC.cmake")
include("${CMAKE_BINARY_DIR}/CMakeRC.cmake")


MACRO(ADD_OPENGL_APP target sources)
   add_executable( ${target} ${sources} )
   message([TRACE] " Add OpenGL executable = ${target} ")

   target_link_libraries( ${target} glfw
                                    OpenGL::GL
                                    ${GLEW_LIB_PATH1}
                                    ${GLEW_LIB_PATH2} )

    IF(MINGW)
        # Statically link against MINGW dependencies
        # for making easier to deploy on other machines. 
        target_link_options( ${target} PRIVATE                                 
                                 -static-libgcc
                                 -static-libstdc++
                                 -Wl,-Bstatic,--whole-archive -lwinpthread
                                   -Wl,--no-whole-archive                                    
                                 )
     ENDIF()       


     # Copy GLEW DLL shared library to same directory as the executable.                 
     IF(WIN32)                           
        add_custom_command(TARGET ${target} POST_BUILD 
                       COMMAND ${CMAKE_COMMAND} -E copy_if_different
                       "${glew-release_SOURCE_DIR}/bin/Release/x64/glew32.dll"              
                       $<TARGET_FILE_DIR:${target}>)
     ENDIF()                                       
ENDMACRO()     


# Download Suzanne 3D model 
file(DOWNLOAD https://raw.githubusercontent.com/alecjacobson/common-3d-test-models/master/data/suzanne.obj
              ${CMAKE_CURRENT_SOURCE_DIR}/suzanne.obj 
              )

# Download Teapot 3D model 
file(DOWNLOAD https://raw.githubusercontent.com/kevinroast/phoria.js/master/teapot.obj
              ${CMAKE_CURRENT_SOURCE_DIR}/teapot.obj 
              )

 # ----------- T A R G E T S ---------------------------$

   ADD_OPENGL_APP( model-loader model-loader.cpp )

   # Define resourse files to be embedded in the executable (model-loader)
   cmrc_add_resource_library( model-loader-resources 
                              NAMESPACE resource
                              ./model_loader_vshader.glsl 
                              ./model_loader_fshader.glsl
                              ./suzanne.obj 
                              ./cube.obj  
                              ./teapot.obj 
                             )

  # Append resources to the executable                            
  target_link_libraries( model-loader
                         model-loader-resources tinyobjloader )


File: model_loader_vshader.glsl / Vertex Shader

  // Vertex Shader Source Code
  #version 330 core 

  layout ( location = 0 ) in vec3 position;
  layout ( location = 1 ) in vec3 normal;  
  layout ( location = 2 ) in vec3 color;   

  uniform mat4 u_model;       // Model matrix 
  uniform mat4 u_view;        // Camera's view matrix    
  uniform mat4 u_projection;  // Camera's projection matrix 
  uniform mat4 u_normal;      // Normal matrix (transpose of view-model inverse)

  varying highp vec4 out_color;
  varying highp vec4 out_light;
  varying vec3 mv_normal, mv_view, mv_position;

  // ------ Pre-defined colors for debugging -----//
   void main()
  {
      mat4 model_view = u_view * u_model; 
      // Should be computed at the application-side 
      /// mat4 normal_mat  = transpose(inverse(model_view));

      // Convert normal to eye space coordinates (aka camera coordinates)
      // mv_normal = normalize( ( normal_mat * vec4(normal, 0.0)).xyz );
      mv_normal = normalize( (  u_normal * vec4(normal, 0.0)).xyz );  

      // Set #if argument to 1 in order to debug the normal
      #if 0
          vec3 _position = position + abs(mv_normal);
      #else 
          vec3 _position = position;
      #endif 

      gl_Position = u_projection * u_view * u_model * vec4(_position, 1.0);        

      // ------------- Lighting -----------------------//

      // Vertex coordinate in eye space (camera-space)
      mv_position = ( model_view * vec4(position, 1.0) ).xyz;

      // out_light = vec4(light, 1.0);
      out_color = vec4(color, 1.0);
}

File: model_loader_fshader.glsl / Fragment Shader

// Fragment Shader Source Code
#version 330
precision mediump float; 

varying highp vec4 out_color;
varying highp vec4 out_light; 
varying vec3 mv_normal, mv_light, mv_view, mv_position;

uniform vec3 u_product_diffuse;
uniform vec3 u_light_position;
uniform vec3 u_product_ambient;

// Specular coefficient 
float spec = 10.0;  

uniform vec3 u_product_specular;

const vec3 color_red   = vec3(1.0, 0.0, 0.0);
const vec3 color_green = vec3(0.0, 1.0, 0.0);  

void main()
{

    vec3 _normal = mv_normal; //;normalize( mv_normal );
    // vec3 light_direction = normalize(vec3(100, 100, 100)); 
    vec3 light_direction = normalize( u_light_position - mv_position );

    float lambert = max( dot(_normal, light_direction), -1.0);

    vec3  light_ambient =  u_product_ambient;
    vec3  light_diffuse =  u_product_diffuse * lambert;         

    // View vector for specular light
    vec3 _view   = normalize( mv_view);
    // Halfway vector
    vec3 H = normalize( light_direction + _view );
    vec3 light_specular = pow( max(dot(H, _normal), 0.0), spec) * u_product_specular; 

    vec4 light = vec4(light_ambient + light_diffuse, 1.0);
    // vec4 light = vec4(light_ambient + light_diffuse + light_specular, 1.0); 

    // gl_FragColor = out_light ; 
    gl_FragColor = light * out_color;

}

File: cube.obj / Cube 3D model Obj file

# cube.obj
#
mtllib cube.mtl
o cube

v -0.500000 -0.500000 0.500000
v 0.500000 -0.500000 0.500000
v -0.500000 0.500000 0.500000
v 0.500000 0.500000 0.500000
v -0.500000 0.500000 -0.500000
v 0.500000 0.500000 -0.500000
v -0.500000 -0.500000 -0.500000
v 0.500000 -0.500000 -0.500000

vt 0.000000 0.000000
vt 1.000000 0.000000
vt 0.000000 1.000000
vt 1.000000 1.000000

vn 0.000000 0.000000 1.000000
vn 0.000000 1.000000 0.000000
vn 0.000000 0.000000 -1.000000
vn 0.000000 -1.000000 0.000000
vn 1.000000 0.000000 0.000000
vn -1.000000 0.000000 0.000000

g cube
usemtl cube
s 1
f 1/1/1 2/2/1 3/3/1
f 3/3/1 2/2/1 4/4/1
s 2
f 3/1/2 4/2/2 5/3/2
f 5/3/2 4/2/2 6/4/2
s 3
f 5/4/3 6/3/3 7/2/3
f 7/2/3 6/3/3 8/1/3
s 4
f 7/1/4 8/2/4 1/3/4
f 1/3/4 8/2/4 2/4/4
s 5
f 2/1/5 8/2/5 4/3/5
f 4/3/5 8/2/5 6/4/5
s 6
f 7/1/6 1/2/6 5/3/6
f 5/3/6 1/2/6 3/4/6

File: model-loader.cpp

#include <iostream>
#include <string> 
#include <sstream>
#include <vector> 
#include <array>
#include <cmath>
#include <functional>
#include <thread>

// -------- OpenGL headers ---------//
//
#define GL_GLEXT_PROTOTYPES 1 // Necessary for OpenGL >= 3.0 functions
#define GL3_PROTOTYPES      1 // Necessary for OpenGL >= 3.0 functions

#if defined(_WIN32)
   #include <windows.h>
   #include <GL/glew.h>
#endif


#include <GL/gl.h>
#include <GLFW/glfw3.h>

// #include <GL/glew.h>
#include <GL/glu.h>

// #include <GL/glut.h>

// --------- OpenGL Math Librar ------------//
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/string_cast.hpp>
#include <glm/gtx/io.hpp>

#include <tiny_obj_loader.h>

#include <cmrc/cmrc.hpp>
CMRC_DECLARE(resource);

struct Position { GLfloat x, y, z;  };
struct Normal   { GLfloat x, y, z;  };
struct Color    { GLfloat r, g, b;  };

struct Vertex   {  
    Position position; // Vertex position in space 
    Normal   normal;   // Vertex normal vector for illumination (Future use)
    Color    color;    // Vertex color (RGB)
};

// Mesh - set of vertices (position and attributes) that represents 
// some surface or solid. 
struct Mesh 
{
    std::vector<Vertex> vertices{};
    std::vector<GLuint> indices{}; 
    GLfloat line_width = 1.0;
    GLenum  draw_type; 
    GLuint vao = 0, vbo = 0, ibo = 0;

    // Draw mesh using indices 
    void draw_indices()
    {
        // Check wheter the mesh was sent to GPU.
        assert( vao != 0 && ibo != 0 );
        // Note: The subroutine glLineWidth is stateful!!
        glLineWidth(line_width);
        glBindVertexArray(vao);
        glDrawElements(draw_type, indices.size(), GL_UNSIGNED_INT, nullptr);
    }

    // Draw mesh using vertices 
    void draw_vertices()
    {
        assert( vao != 0 );
        glLineWidth(line_width);       
        glBindVertexArray(vao);
        glDrawArrays(draw_type, 0, vertices.size());
    }
};


GLFWwindow* make_glfwWindow(int  width, int height, const char* title);
void         compile_shader(GLuint m_program, const char* code, GLenum type);
void              send_mesh( Mesh& mesh , GLint attr_position , GLint attr_normal, GLint attr_color); 

// Load  mesh data from obj file, often generated by tools such as Blender or 3D studio max.
Mesh load_model(const std::string& file, const Color& color);

void set_uniform_matrix(GLint uniform, glm::mat4 const& m)
{
    glUniformMatrix4fv(uniform, 1, GL_FALSE, glm::value_ptr(m) );
}

// Get shader uniform location and check return status code.
GLint shader_uniform(GLuint program, const char* name);
// Get shader attribute location and return status code.
GLint shader_attr(GLint program, const char* name);

// Get content of resource file, embedded in the executable file, as string.
// Requires:  #include <cmrc/cmrc.hpp> 
std::string get_resource(std::string file_name);

constexpr Normal zero_normal = { 0.0, 0.0 , 0.0};
constexpr Color  color_red   = {1.0, 0.0, 0.0};
constexpr Color  color_green = {0.0, 1.0, 0.0};        
constexpr Color  color_blue  = {0.0, 0.0, 1.0};
constexpr Color color_gold  = {1.0, 0.843, 0.0};

const glm::mat4 matrix_identity = glm::mat4(1.0);


struct Camera
{
    // Current location of camera in world coordinates
    glm::vec3 eye  = { 2.0, 4.0, 5.0 };
    // Direction to where camera is looking at.
    glm::vec3 forward =  glm::normalize(glm::vec3{-1.0, 0.0, -1.0 });
    // Current  camera up vector (orientation) - Y axis (default)
    glm::vec3 up = { 0.0, 1.0, 0.0 };

    // Field of view
    float fov_angle = glm::radians(60.0);
    // Aspect ratio
    float aspect   = 1.0;
    float zFar     = 100.0;
    float zNear    = 0.1;

    // ID of shader's view uniform variable - for setting view matrix
    GLint shader_uniform_view = -1;
    // ID of shader's projection uniform variable for setting projection matrix.
    GLint shader_uniform_proj = -1;

    Camera(GLuint uniform_view, GLuint uniform_proj, GLfloat aspect):
         shader_uniform_view(uniform_view)
       , shader_uniform_proj(uniform_proj)
       , aspect(aspect)
    {  update_view();  }

    void update_view()
    {
        // Point to where camera is looking at.
        auto cam_at = this->eye + this->forward;
        std::cerr << "  [CAMERA] Camera coordinate (eye)        = " << eye << '\n';
        std::cerr << "  [CAMERA] Looking at direction (forward) = " << forward << '\n'; 

        // Create View matrix (maps from world-space to camera-space)
        auto Tview = glm::lookAt(eye, cam_at, up);
        // Create projection matrix maps - camera-space to NDC (Normalized Device Coordinates).
        auto Tproj = glm::perspective( fov_angle, aspect, zNear, zFar );
        // Set shader uniform variables.
        glUniformMatrix4fv(shader_uniform_view, 1, GL_FALSE, glm::value_ptr(Tview) );
        glUniformMatrix4fv(shader_uniform_proj, 1, GL_FALSE, glm::value_ptr(Tproj) );

    }

    // Compute projection matrix 
    glm::mat4 get_proj()
    {   return glm::perspective( fov_angle, aspect, zNear, zFar ); }

    //  Compute view matrix 
    glm::mat4 get_view() 
    {
        // Point to where camera is looking at.
        auto cam_at = this->eye + this->forward;
        auto Tview = glm::lookAt(eye, cam_at, up);
        return Tview;
    }
    // Compute normal view matrix for illumination
    glm::mat4 normal_view(glm::mat4 const& model)
    {
        return glm::transpose( glm::inverse( this->get_view() * model ) );
    }

    // Rotate around camera's Up vecto.
    void rotate_yaw(float angle)
    {
        // Build quaternion from axis angle
        glm::quat q = glm::angleAxis(glm::radians(angle), this->up);
        // Rotate current forward vector
        forward = forward * q;
        // std::cout << " [FORWARD VECTOR ] " << glm::to_string(forward) << '\n';
        this->update_view();
    }

    // Rotate around camera's pitch axis (X axis) elevate camera view.
    void rotate_pitch(float angle)
    {
        glm::vec3 axis = glm::cross(this->up, this->forward);
        // Build quaternion from axis angle
        glm::quat q = glm::angleAxis(glm::radians(angle), axis);
        forward = q * forward;
        this->update_view();
    }

      // Move at forward vector direction (to where camera is looking at).
    void move_forward(float factor)
    {
        this->eye = this->eye + factor * this->forward;
        this->update_view();
    }
    // Move camera to specific point in the space.
    void set_position(float x, float y, float z)
    {
        this->eye = glm::vec3(x, y, z);
        this->update_view();
    }

    // Set position to where camera is looking at.
    void look_at(const glm::vec3& at)
    {
        this->forward = glm::normalize(at - this->eye);

        this->update_view();
    }

};


// Create mesh for plane (represented by a square) on origin
Mesh make_plane_mesh(float side, glm::vec3 const& VA, glm::vec3 const& VB
                    , Normal const& normal, Color const& color)
{
    glm::vec3 B = side * VA, C = side * VB, D = B  + C;
    Position pA = {0, 0, 0}, pB = {B[0], B[1], B[2]}, pC = {C[0], C[1], C[2]}, pD = {D[0], D[1], D[2]} ;
    Mesh mesh_plane;
    mesh_plane.draw_type = GL_TRIANGLES;
    mesh_plane.line_width = 1.0;

    mesh_plane.vertices = {
        // ----- Triagle 1 ------------// 
          { pA, normal, color },{ pB, normal, color }, { pC, normal, color }
        // ----- Triangle 2 -------------//
        , { pB, normal, color }, { pC, normal, color } , Vertex{ pD, normal, color } 
     };  

    return mesh_plane;
}

int main(int argc, char** argv)
{
    // glutInit(&argc, argv);

    if (!glfwInit()){ return -1; }
    GLFWwindow* window = make_glfwWindow(640, 480, "Load 3D models. ");

    // ====== S H A D E R - C O M P I L A T I O N ====//
    //                                                //     
    // Note: The shader source code is at the end of file.
    extern const char* code_vertex_shader;
    extern const char* code_frag_shader;

    GLuint prog = glCreateProgram();

    std::string vertex_shader   = get_resource("/model_loader_vshader.glsl");
    std::string fragment_shader = get_resource("/model_loader_fshader.glsl");

    // Compile shader code 
    compile_shader(prog, vertex_shader.c_str(),  GL_VERTEX_SHADER  ) ;    
    compile_shader(prog, fragment_shader.c_str(), GL_FRAGMENT_SHADER );
    glUseProgram(prog);

    const GLint attr_position = shader_attr(prog, "position");
    const GLint attr_normal   = shader_attr(prog, "normal");   
    const GLint attr_color    = shader_attr(prog, "color");

    const GLint u_proj        = shader_uniform(prog, "u_projection");
    const GLint u_view        = shader_uniform(prog, "u_view");
    const GLint u_model       = shader_uniform(prog, "u_model");
    const GLint u_normal      = shader_uniform(prog, "u_normal");
    const GLint u_product_ambient  = shader_uniform(prog,  "u_product_ambient");
    const GLint u_product_diffuse  = shader_uniform(prog,  "u_product_diffuse");
    const GLint u_product_specular = shader_uniform(prog, "u_product_specular");
    const GLint u_light_position   = shader_uniform(prog, "u_light_position");

    set_uniform_matrix(u_proj,   matrix_identity);
    set_uniform_matrix(u_view,   matrix_identity);
    set_uniform_matrix(u_model,  matrix_identity);
    set_uniform_matrix(u_normal, matrix_identity); 

    // Ambient light RGB colors  
    glm::vec3 La = {0.0, 1.0, 0.5};
    // Diffuse light RGB colors 
    glm::vec3 Ld = {0.6,  0.4, 0.9};
    // Specular light RGB colors 
    glm::vec3 Ls = {1.0, 1.0, 1.0};

    // ---- Material coefficients ------//
    glm::vec3 ka = {0.5, 0.2, 1.0};
    glm::vec3 kd = {0.8, 1.0, 1.0};
    glm::vec3 ks = {0.4, 0.3, 0.2};
    glm::vec3 light_position = { 1.0, 1.0, 1.0 };

    std::cout << "  [TRACE] Ambient =>> ka * La = " << (ka * La) << '\n';
    std::cout << "  [TRACE] Diffuse =>> kd * Ld = " << (kd * Ld) << '\n';

    // Note: The  product ka * La is an element-wise product.
    // It is not a dot product or cross product.
    glUniform3fv(u_light_position,   1, glm::value_ptr( light_position ) );
    glUniform3fv(u_product_ambient,  1, glm::value_ptr( ka * La ));
    glUniform3fv(u_product_diffuse,  1, glm::value_ptr( kd * Ld ));
    glUniform3fv(u_product_specular, 1, glm::value_ptr( ks * Ls ));    

    Mesh mesh_planeXZ = make_plane_mesh( 10.0, glm::vec3{1.0, 0.0, 0.0}
                                    , glm::vec3{0.0, 0.0, 1.0}, Normal{0.0, 1.0, 0.0}, color_blue );
    send_mesh(mesh_planeXZ, attr_position, attr_normal, attr_color);

    Mesh mesh_planeXY = make_plane_mesh( 10.0, glm::vec3{0.0, 1.0, 0.0}
                                    , glm::vec3{1.0, 0.0, 0.0}, Normal{0.0, 0.0, 1.0}, color_green );
    send_mesh(mesh_planeXY, attr_position, attr_normal, attr_color);


    Mesh mesh_suzanne = load_model("/suzanne.obj", Color{0.51, 0.56, 0.50}); 
    send_mesh(mesh_suzanne, attr_position, attr_normal,  attr_color);
    glm::mat4 transform_suzanne =  matrix_identity;
    transform_suzanne = glm::translate( transform_suzanne, -glm::vec3( -1.182, 1.306, 3.573) );
    transform_suzanne = glm::translate( transform_suzanne, +glm::vec3( 4.0, 3.0, 5.0) );  
    // model_matrix = glm::rotate( model_matrix, glm::radians(45.0f), glm::vec3(0, 0, 1));

    Mesh mesh_cube = load_model("/cube.obj", color_gold);
    send_mesh(mesh_cube, attr_position, attr_normal, attr_color);
    glm::mat4 transform_cube = matrix_identity;
    transform_cube = glm::translate( transform_cube, glm::vec3(4.0, 2.0, 5.0) );
    transform_cube = glm::rotate( transform_cube, glm::radians(35.0f), glm::vec3(1.0, 1.0, 2.0) );
    transform_cube = glm::scale( transform_cube, glm::vec3(1.2, 1.2, 1.2));


    Mesh mesh_teapot = load_model("/teapot.obj", color_red);
    send_mesh(mesh_teapot, attr_position, attr_normal, attr_color);
    glm::mat4 transform_teapot = matrix_identity;
    transform_teapot = glm::translate( transform_teapot, glm::vec3(5.0, 5.0, 5.0) );
    transform_teapot = glm::rotate( transform_teapot, glm::radians(50.0f), glm::vec3(3.0, 1.0, 5.0) );
    transform_teapot = glm::scale( transform_teapot, glm::vec3(1.0 / 8.0, 1.0 / 8.0, 1.0 / 8.0));


    int width, height;
    glfwGetWindowSize(window, &width, &height);
    // Window aspect ratio
    float aspect = static_cast<float>(width) / height;


    // Set projection matrix uniform variable
    // glUniformMatrix4fv(u_proj, 1, GL_FALSE, glm::value_ptr(identity) );

    Camera camera(u_view, u_proj, width / height);
    camera.set_position(5.236, 4.078, 11.610);
    camera.look_at( { 4.0, 1.4, 5.166} );

    // Set model matrix and update the normal matrix for 
    // scene illumination calculations. 
    auto set_model_matrix = [&camera, u_normal, u_model](const glm::mat4& model_transform)
    {
        set_uniform_matrix(u_model, model_transform);
        glm::mat4 normal_matrix = glm::transpose( glm::inverse(camera.get_view() * model_transform) );
        set_uniform_matrix(u_normal, normal_matrix);
    };

    //  ======= R E N D E R  - L O O P ============//
    //                                             //
    while ( !glfwWindowShouldClose(window) )
    {
        glClear(GL_COLOR_BUFFER_BIT  | GL_DEPTH_BUFFER_BIT);

        // ====== BEGIN RENDERING ============//    
            // set_uniform_matrix(u_model, matrix_identity);
            set_model_matrix(matrix_identity);
            mesh_planeXZ.draw_vertices();

            // set_uniform_matrix(u_model, matrix_identity);
            set_model_matrix(matrix_identity);
            mesh_planeXY.draw_vertices();           

            set_uniform_matrix(u_model, transform_suzanne);
            mesh_suzanne.draw_vertices();

            set_uniform_matrix(u_model, transform_cube);
            mesh_cube.draw_vertices();          

            set_uniform_matrix(u_model, transform_teapot);
            mesh_teapot.draw_vertices();

        // ====== END RENDERING ==============//

         // Move camera to direction to where it is looking at.
        if( glfwGetKey(window, GLFW_KEY_UP ) == GLFW_PRESS    ){ camera.move_forward(+2.0);  }
        if( glfwGetKey(window, GLFW_KEY_DOWN ) == GLFW_PRESS  ){ camera.move_forward(-2.0);  }

        // Rotate around its Y axis 
        if( glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS  ){ camera.rotate_yaw(+5.0);   }
        if( glfwGetKey(window, GLFW_KEY_LEFT ) == GLFW_PRESS  ){ camera.rotate_yaw(-5.0);   }

        // Rotate camera around its local X axis
        if( glfwGetKey(window, 'S') == GLFW_PRESS ){ camera.rotate_pitch(+5.0); }
        if( glfwGetKey(window, 'D') == GLFW_PRESS ){ camera.rotate_pitch(-5.0); }

        if( glfwGetKey(window, 'H') == GLFW_PRESS )
        {
            // Rotate around Z axis
            transform_suzanne = glm::rotate(transform_suzanne, glm::radians(10.0f), glm::vec3(0, 0, 1));
        }
        if( glfwGetKey(window, 'J') == GLFW_PRESS )
        {
            // Rotate around Z axis
            transform_suzanne = glm::rotate(transform_suzanne, glm::radians(10.0f), glm::vec3(1, 0, 0));
        }

        // Reset camera - look at origin
        if( glfwGetKey(window, 'R' ) == GLFW_PRESS  )
        {
            camera.set_position(4.0, 3.0, 2.5);
            camera.look_at( {0.0, 0.0, 0.0} );
        }

        // glm::mat4 normal_view_matrix = camera.normal_view( model_matrix ); 
        // set_uniform_matrix( u_normalview, normal_view_matrix );


        /* Swap front and back buffers */
        glfwSwapBuffers(window);
        /* Poll for and process events */
        glfwWaitEvents();

    } // --- End while() --- //

    glfwTerminate();

    return 0;
}


// ---------------------------------------//

GLFWwindow* 
make_glfwWindow(int  width, int height, const char* title)
{

    glfwSetErrorCallback([](int error, const char* description)
                         { std::fprintf( stderr, " [GLFW ERROR] Error = %d ; Description = %s \n"
                                        , error, description);
                         });

    GLFWwindow* window = glfwCreateWindow(width, height, title, NULL, NULL);
    assert( window != nullptr && "Failed  to create Window");

    // OpenGL context 
    glfwMakeContextCurrent(window);
    // Pain whole screen as black - dark screen colors are better 
    // for avoding eye strain due long hours staring on monitor.
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    // Set - OpenGL Core Profile - version 3.3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    glEnable(GL_COLOR_MATERIAL);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);

    // --- Initialize GLEW library after OpenGL context --------- //
    #if _WIN32
    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK) {
        std::cerr << "GLEW::Error : failed to initialize GLEW" << std::endl;
        std::abort();
    }
    std::fprintf(stderr, " [TRACE] Glew Initialized Ok. \n");
    #endif


    return window;
}


// Get shader uniform location and check error.
GLint shader_uniform(GLuint program, const char* name)
{
    GLint out = glGetUniformLocation(program, name);
    if(out < 0){ std::fprintf(stderr, " [SHADER ERROR] Uniform location not found = %s \n", name); }
    return out;
}

GLint shader_attr(GLint program, const char* name)
{
    GLint out = glGetAttribLocation(program, name);
    if(out < 0){ std::fprintf(stderr, " [SHADER ERROR] Attribute location not found = %s \n", name); }
    return out;
}



void compile_shader(GLuint m_program, const char* code, GLenum type)
{
    GLint shader_id = glCreateShader( type );
    glShaderSource(shader_id, 1, &code, nullptr);
    glCompileShader(shader_id);
    GLint is_compiled = GL_FALSE;
    // Check shader compilation result 
    glGetShaderiv(shader_id, GL_COMPILE_STATUS, &is_compiled);

    const std::string shader_type = [&]{
        if(type == GL_VERTEX_SHADER   ){ return "VERTEX_SHADER"; }
        if(type == GL_FRAGMENT_SHADER ){ return "FRAGMENT_SHADER"; }
        return "UNKNOWN"; 
    }();

    // If there is any shader compilation result, 
    // print the error message.
    if( is_compiled == GL_FALSE)
    {
        GLint length;
        glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &length);
        assert( length > 0 );

        std::string out(length + 1, 0x00);
        GLint chars_written;
        glGetShaderInfoLog(shader_id, length, &chars_written, out.data());
        std::cerr << " [SHADER ERROR] = " << " (" << shader_type << " ) =>> " << out << '\n';
        // Abort the exection of current process. 
        std::abort();
    }          

    glAttachShader(m_program, shader_id);   
    glDeleteShader(shader_id);
    glLinkProgram(m_program);
    GLint link_status = GL_FALSE;
    glGetProgramiv(m_program, GL_LINK_STATUS, &link_status);
    assert( link_status != GL_FALSE );   
    // glUseProgram(m_program);         
}


//  Send mesh data to GPU  ----//
void send_mesh( Mesh& mesh , GLint attr_position , GLint attr_normal, GLint attr_color ) 
{ 
    GLuint& vao = mesh.vao;
    GLuint& vbo = mesh.vbo;
    GLuint& ibo = mesh.ibo;

    // ------------- Upload vertices --------------//

    // Generate and bind current VAO (Vertex Array Object)
    if(vao == 0){ glGenVertexArrays(1, &vao); }
    glBindVertexArray(vao);
    // Generate and bind current VBO (Vertex Buffer Object)
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);

    // Upload vertex attributes (position, normal vector and color ) to GPU in a single call. 
    glBufferData(GL_ARRAY_BUFFER, mesh.vertices.size() * sizeof(Vertex)
                , mesh.vertices.data(), GL_STATIC_DRAW);   

    // Set data layout - how data will be interpreted.
    // => Each vertex has 3 coordinates.
    glEnableVertexAttribArray(attr_position); 
    glVertexAttribPointer( attr_position, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);

    #if 1 
    glEnableVertexAttribArray(attr_normal);          
    glVertexAttribPointer( attr_normal, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex)     
                           // Offset to color member variable in class Vertex
                          , reinterpret_cast<void*>( offsetof(Vertex, normal) )
                          );
    #endif 

    #if 1
    glEnableVertexAttribArray(attr_color);    
    glVertexAttribPointer( attr_color, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex)     
                           // Offset to color member variable in class Vertex
                          , reinterpret_cast<void*>( offsetof(Vertex, color) )
                          );
    #endif 

    // -------- Upload indices ----------------------//
    //
    if (!mesh.indices.empty())
    {
        glBindVertexArray(vao);
        glGenBuffers(1, &ibo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, mesh.indices.size() * sizeof(GLuint)
                     , mesh.indices.data(), GL_STATIC_DRAW );
    }

    // ------ Disable Global state set by this function -----//
    // Unbind VAO 
    glBindVertexArray(0);
    // Unbind VBO 
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // Unbind IBO 
    glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0);
    // Disable current shader attribute 
    glDisableVertexAttribArray(attr_position);
    glDisableVertexAttribArray(attr_normal);
    glDisableVertexAttribArray(attr_color);

}

// Load mesh from embedded resource file 
Mesh load_model(const std::string& file, const Color& color)
{
    auto reader_config = tinyobj::ObjReaderConfig{};
    reader_config.mtl_search_path = "./";
    auto reader = tinyobj::ObjReader{};

    std::string data = get_resource(file);

    // Parse from file:
    // if( !reader.ParseFromString(file, reader_config) || !reader.Error().empty() 
    if( !reader.ParseFromString(data, "", reader_config) || !reader.Error().empty() )
    {
        std::cerr << " [ERROR] " << reader.Error() << '\n';
        std::abort();
    }
    if( !reader.Warning().empty() )
    {
        std::cerr << " [WARNING] TinyObjReader = " << reader.Warning();
    }
    auto &attrib = reader.GetAttrib();
    auto &shapes = reader.GetShapes();
    auto &materials = reader.GetMaterials();
    assert( shapes.size() >= 1 && "It needs at least one shape." );

    Mesh mesh;
    mesh.draw_type = GL_TRIANGLES;

    size_t index_offset = 0;
    int i = 0;

    // Positive infinity point 
    glm::vec3 current = {0.0, 0.0, 0.0}, a = {2.0 / 0.0, 2.0, 2.0 / 0.0};

    // Loop over face polygon 
    for( size_t f  = 0; f < shapes[0].mesh.num_face_vertices.size(); f++ )
    {
        int fv = shapes[0].mesh.num_face_vertices[f];
        // Loop over vertices in the face 
        for(size_t v = 0; v < fv; v++)
        {
            tinyobj::index_t idx = shapes[0].mesh.indices[index_offset + v];
            tinyobj::real_t vx = attrib.vertices[3 * idx.vertex_index + 0];
            tinyobj::real_t vy = attrib.vertices[3 * idx.vertex_index + 1];
            tinyobj::real_t vz = attrib.vertices[3 * idx.vertex_index + 2];

            tinyobj::real_t nx = attrib.normals[3 * idx.normal_index + 0];
            tinyobj::real_t ny = attrib.normals[3 * idx.normal_index + 1];
            tinyobj::real_t nz = attrib.normals[3 * idx.normal_index + 2];
            // Add vertex to mesh 
            mesh.vertices.push_back({ Position{vx, vy, vz}, Normal{nx , ny, nz}, color});
            current = {vx, vy, vz};
            if( glm::length( current ) < glm::length(a)){ a = current; }
            // std::fprintf(stderr, " [TRACE] i = %d; x = %f ; y = %f ; z = %f \n", i++, vx, vy, vz); 
            // std::fprintf(stderr, " [TRACE] i = %d; nx = %f ; ny = %f ; nz = %f \n", i++, nx, ny, nz); 
        }

        index_offset += fv;
        shapes[0].mesh.material_ids[f];
    }

    std::cout << " [DEBUG] Point with minimum distance from origin = " << a << '\n';
    return mesh;
}

std::string 
get_resource(std::string file_name)
{
    std::cout << " [TRACE] Before file " << '\n';
    auto fs = cmrc::resource::get_filesystem();
    std::cout << " [TRACE] Resource = " << fs.is_file(file_name ) << '\n';
    assert( fs.is_file(file_name) && "Unable to resource find file."  );
    auto fd = fs.open(file_name);
    return std::string(fd.begin(), fd.end());
}

Building on Linux

$ cmake -H. -B_build_linux -DCMAKE_BUILD_TYPE=Debug
$ cmake --build _build_linux --target

Cross-compiling for Windows from Linux via MINGW Docker

$ docker run -it --rm -v $PWD:/cwd -w /cwd --entrypoint=bash dockcross/windows-static-x64
$ cmake -H. -B_build_windows -DCMAKE_BUILD_TYPE=Debug
$ cmake --build _build_windows --target

Created: 2024-08-12 Mon 23:54

Validate