Learn Ray Casting – Working Example – Using OpenGL

In this tutorial, I explain exactly what ray casting is. You'll get to see my cool working example which is running the source code as available to you listed on this page. Mouse picking is another name for ray casting, so if you're looking to learn that, then this video is for you also.

Note: The video will be published in the next few days, but if you're watching this on YouTube, then it'll already be appearing here!

  1. You'll learn how to cast a ray from screen space, to the near plane of the view frustum, through 3D space
  2. You'll learn how to calculate the intersect point where the ray passes through a sphere, and we'll compare that with “glm::unProject”
  3. You'll learn how to transform the position of the sphere in 3D space, along the X, Y, and Z axes, by using the mouse
  4. You'll learn how to create a coloured and transparent hotspot on the sphere's surface by using shaders

Source code: C++... main.cpp

File: main.cpp
  1. #include <glad/glad.h>
  2. #include <GLFW/glfw3.h>
  3.  
  4. #define STB_IMAGE_IMPLEMENTATION
  5. #include "stb_image.h"
  6.  
  7. // OpenGL Mathematics(GLM)
  8. // -----------------------
  9. #include <glm/glm.hpp>
  10. #include <glm/gtc/matrix_transform.hpp>
  11. #include <glm/gtc/type_ptr.hpp>
  12.  
  13. #include <assimp/Importer.hpp>
  14. #include <assimp/scene.h>
  15. #include <assimp/postprocess.h>
  16.  
  17. #include <vector>
  18. #include <iostream>
  19. #include <fstream>
  20.  
  21. #include "load_model_meshes.h"
  22. #include "shader_configure.h"
  23.  
  24. double mouse_wheel_val = 0;
  25. void mouse_wheel_callback(GLFWwindow* window, double xoffset, double yoffset)
  26. {
  27.     mouse_wheel_val = yoffset;
  28. }
  29.  
  30. int main()
  31. {
  32.     // (1) GLFW: Initialise & Configure
  33.     // --------------------------------
  34.     if (!glfwInit())
  35.         exit(EXIT_FAILURE);
  36.  
  37.     glfwWindowHint(GLFW_SAMPLES, 4); // Anti-aliasing (applies to glEnable(GL_MULTISAMPLE) further down)
  38.     glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
  39.     glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
  40.     glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  41.  
  42.     const GLFWvidmode* mode = glfwGetVideoMode(glfwGetPrimaryMonitor());
  43.  
  44.     int monitor_width = mode->width; // Monitor's width
  45.     int monitor_height = mode->height;
  46.  
  47.     int window_width = (int)(monitor_width * 0.9f); // Set window size to % of monitor size
  48.     int window_height = (int)(monitor_height * 0.9f);
  49.  
  50.     GLFWwindow* window = glfwCreateWindow(window_width, window_height, "OpenGL Ray Casting - Mouse Picking Technique", NULL, NULL);
  51.  
  52.     if (!window)
  53.     {
  54.         glfwTerminate();
  55.         exit(EXIT_FAILURE);
  56.     }
  57.     glfwMakeContextCurrent(window); // Set the window to be used
  58.     glfwSetWindowPos(window, (monitor_width - window_width) / 2, (monitor_height - window_height) / 2); // Centre the window
  59.  
  60.     glfwSwapInterval(1); // Set VSync rate 1:1 with monitor's refresh rate
  61.     glfwSetScrollCallback(window, mouse_wheel_callback);
  62.  
  63.     // (2) GLAD: Load OpenGL Function Pointers
  64.     // ---------------------------------------
  65.     if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
  66.     {
  67.         glfwTerminate();
  68.         exit(EXIT_FAILURE);
  69.     }
  70.     glEnable(GL_DEPTH_TEST);
  71.     glEnable(GL_MULTISAMPLE);
  72.     glEnable(GL_BLEND);
  73.     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  74.  
  75.     // (3) Compile Shaders Read from Text Files
  76.     // ----------------------------------------
  77.     const char* vert_shader = "../../Shaders/shader_glsl.vert";
  78.     const char* frag_shader = "../../Shaders/shader_glsl.frag";
  79.  
  80.     Shader shader(vert_shader, frag_shader);
  81.     shader.use();
  82.  
  83.     unsigned int view_matrix_loc = glGetUniformLocation(shader.ID, "view");
  84.     unsigned int projection_matrix_loc = glGetUniformLocation(shader.ID, "projection");
  85.  
  86.     unsigned int camera_position_loc = glGetUniformLocation(shader.ID, "camera_position");
  87.     unsigned int image_sampler_loc = glGetUniformLocation(shader.ID, "image");
  88.  
  89.     // (4) Load Model & Set Camera
  90.     // ---------------------------
  91.     Model plane("Radius 2 Size Sphere.obj");
  92.  
  93.     glm::vec3 camera_position(0.0f, 0.0f, 5.0f); // -Z is into the screen
  94.     glm::vec3 camera_target(0.0001f, 0.0f, -1.0f);
  95.     glm::vec3 camera_up(0.0f, 1.0f, 0.0f);
  96.  
  97.     glUniform3f(camera_position_loc, camera_position.x, camera_position.y, camera_position.z);
  98.  
  99.     glActiveTexture(GL_TEXTURE0); // Reusing the same texture unit for each model mesh
  100.     glUniform1i(image_sampler_loc, 0);
  101.  
  102.     glm::mat4 view = glm::lookAt(camera_position, camera_target, camera_up);
  103.     glUniformMatrix4fv(view_matrix_loc, 1, GL_FALSE, glm::value_ptr(view)); // Transfer view matrix to vertex shader uniform
  104.  
  105.     constexpr float FOV = glm::radians(70.0f);
  106.     float near_plane = 0.5f;
  107.  
  108.     glm::mat4 projection = glm::perspective(FOV, (float)window_width / (float)window_height, near_plane, 75.0f);
  109.     glUniformMatrix4fv(projection_matrix_loc, 1, GL_FALSE, glm::value_ptr(projection));
  110.  
  111.     // (5) Setup Random Values
  112.     // -----------------------
  113.     /* srand((unsigned)time(NULL)); // Initialise random seed
  114.  
  115.     float x_spin = 1.0f / (rand() % 10 + 1); // Generate random number between 1 and 10
  116.     float y_spin = 1.0f / (rand() % 10 + 1);
  117.     float z_spin = 1.0f / (rand() % 10 + 1);
  118.     float spin_speed = (float)(rand() % 3 + 1);
  119.  
  120.     float spin_vary = 0.0f;
  121.     int spin_dir = 1; */
  122.  
  123.     // (6) Sphere Variables
  124.     // --------------------
  125.     unsigned int sphere_trans_mat_loc = glGetUniformLocation(shader.ID, "sphere_trans_mat");
  126.     unsigned int ray_intersect_point_loc = glGetUniformLocation(shader.ID, "ray_intersect_point");
  127.  
  128.     float sphere_radius = 2.0f;
  129.  
  130.     glm::mat4 sphere_rot_mat(1.0f);
  131.     glm::mat4 sphere_pos_mat(1.0f);
  132.     glm::mat4 sphere_trans_mat(1.0f);
  133.     sphere_pos_mat = glm::translate(sphere_pos_mat, glm::vec3(0.0f, 0.0f, -7.0f)); // Change sphere position from Blender 0,0,0
  134.  
  135.     while (!glfwWindowShouldClose(window)) // Main Loop
  136.     {
  137.         // (7) Ray Projection - Part 1 (Casting the Ray)
  138.         // ---------------------------------------------
  139.         double mousePosX, mousePosY;
  140.         glfwGetCursorPos(window, &mousePosX, &mousePosY);
  141.  
  142.         // Casting the Ray Ourselves
  143.         // -------------------------
  144.         float NDC_X = ((int)mousePosX * (2.0f / window_width)) - 1;
  145.         float NDC_Y = -((int)mousePosY * (2.0f / window_height)) + 1; // GLFW is mouse Y at top = 0... so OpenGL (GLM) needs it negating
  146.  
  147.         float near_plane_height = glm::tan(FOV / 2.0f) * near_plane;
  148.         float aspect_ratio = (float)window_width / (float)window_height;
  149.  
  150.         float X_3D = NDC_X * near_plane_height * aspect_ratio;
  151.         float Y_3D = NDC_Y * near_plane_height;
  152.  
  153.         glm::vec3 near_plane_point(X_3D, Y_3D, -near_plane); // Point is now in accordance with the camera in 3D space on the near plane
  154.         near_plane_point = glm::inverse(view) * glm::vec4(near_plane_point, 1.0f); // Point true position in 3D space
  155.         glm::vec3 cam_dir_vec = near_plane_point - camera_position;
  156.  
  157.         // Casting the Ray with glm::unProject
  158.         // -----------------------------------
  159.         glm::vec3 unProject = glm::unProject(glm::vec3(mousePosX, window_height - mousePosY, 0.0f), view, projection, glm::vec4(0, 0, window_width, window_height));
  160.         glm::vec3 unProj_dir_vec = unProject - camera_position;
  161.  
  162.         /*std::cout << "\n cam_dir_vec.x = " << cam_dir_vec.x << " -- unProj_dir_vec.x = " << unProj_dir_vec.x << " -- cam_dir_vec.y = " << cam_dir_vec.y
  163.             << " -- unProj_dir_vec.y = " << unProj_dir_vec.y << " -- cam_dir_vec.z = " << cam_dir_vec.z << " -- unProj_dir_vec.z = " << unProj_dir_vec.z;*/
  164.  
  165.         glm::vec3 ray_direction = glm::normalize(cam_dir_vec);
  166.         //glm::vec3 ray_direction = glm::normalize(unProj_dir_vec);
  167.  
  168.  
  169.         // (8) Ray Projection - Part 1B (Transforming the Sphere)
  170.         // ------------------------------------------------------
  171.         // Option 1: Mouse Control
  172.         // -----------------------
  173.         float near_Z_to_sphere_Z = sphere_pos_mat[3][2] - near_plane_point.z; // [3][2] = sphere Z value
  174.         float ray_multiple_to_sphere_Z = near_Z_to_sphere_Z / ray_direction.z;
  175.  
  176.         glm::vec3 ray_point_at_sphere_Z = near_plane_point + (ray_direction * ray_multiple_to_sphere_Z);
  177.         //std::cout << "\n ray_point_at_sphere_Z.x = " << ray_point_at_sphere_Z.x << " -- ray_point_at_sphere_Z.y = " << ray_point_at_sphere_Z.y;
  178.  
  179.         float difference_X = ray_point_at_sphere_Z.x - sphere_pos_mat[3][0];
  180.         float difference_Y = ray_point_at_sphere_Z.y - sphere_pos_mat[3][1];
  181.  
  182.         sphere_pos_mat = glm::translate(sphere_pos_mat, glm::vec3(difference_X * 0.03f, difference_Y * 0.03f, mouse_wheel_val * 0.5f));
  183.         mouse_wheel_val = 0;
  184.  
  185.         sphere_trans_mat = sphere_pos_mat;
  186.         glUniformMatrix4fv(sphere_trans_mat_loc, 1, GL_FALSE, glm::value_ptr(sphere_trans_mat));
  187.  
  188.         // Option 2: Random Orbits
  189.         // -----------------------
  190.         //spin_vary += 0.05f * spin_dir;
  191.  
  192.         //if (spin_vary > 6 || spin_vary < 0)
  193.         //{
  194.         //    spin_dir = -spin_dir; // Reverse the spinning direction
  195.  
  196.         //    x_spin = 1.0f / (rand() % 10 + 1);
  197.         //    y_spin = 1.0f / (rand() % 10 + 1);
  198.         //    z_spin = 1.0f / (rand() % 10 + 1);
  199.         //    spin_speed = (float)(rand() % 3 + 1);
  200.         //}
  201.         //sphere_rot_mat = glm::rotate(sphere_rot_mat, glm::radians(spin_speed), glm::vec3(x_spin, y_spin, z_spin));
  202.         //sphere_trans_mat = sphere_rot_mat * sphere_pos_mat; // Global transform
  203.         //glUniformMatrix4fv(sphere_trans_mat_loc, 1, GL_FALSE, glm::value_ptr(sphere_trans_mat));
  204.  
  205.  
  206.         // (9) Ray Projection - Part 2 (Calculating the Sphere Intersection Point)
  207.         // -----------------------------------------------------------------------
  208.         glm::vec3 sphere_position = sphere_trans_mat * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);
  209.         glm::vec3 camera_to_sphere_centre = sphere_position - camera_position;
  210.  
  211.         float main_angular_difference_dot = glm::dot(ray_direction, glm::normalize(camera_to_sphere_centre));
  212.         float main_angular_difference_radians = glm::max(0.00001f, glm::acos(main_angular_difference_dot));
  213.         float main_angular_difference_degrees = glm::degrees(main_angular_difference_radians);
  214.         //std::cout << "\n main_angular_difference_degrees = " << main_angular_difference_degrees;
  215.  
  216.         float camera_to_sphere_length = glm::length(camera_to_sphere_centre);
  217.         float camera_to_sphere_opposite = glm::sin(main_angular_difference_radians) * camera_to_sphere_length;
  218.  
  219.         float small_triangle1_radians = glm::asin(camera_to_sphere_opposite / sphere_radius);
  220.         float small_triangle1_degrees = glm::degrees(small_triangle1_radians);
  221.  
  222.         float angle_180_deg_difference = 180.0f - small_triangle1_degrees;
  223.         float small_triangle2_degrees = 180.0f - angle_180_deg_difference - main_angular_difference_degrees;
  224.         float small_triangle2_opposite = glm::sin(glm::radians(small_triangle2_degrees)) * sphere_radius;
  225.  
  226.         float ray_intersect_length = small_triangle2_opposite / glm::sin(main_angular_difference_radians);
  227.         glm::vec3 ray_intersect_point = camera_position + (ray_direction * ray_intersect_length);
  228.  
  229.         /*std::cout << "\n ray_intersect_point.x = " << ray_intersect_point.x << " --- ray_intersect_point.y = " << ray_intersect_point.y
  230.         << " --- ray_intersect_point.z = " << ray_intersect_point.z;*/
  231.  
  232.         glUniform3f(ray_intersect_point_loc, ray_intersect_point.x, ray_intersect_point.y, ray_intersect_point.z);
  233.  
  234.  
  235.         // (10) Clear the Screen & Draw Model Meshes
  236.         // -----------------------------------------
  237.         glClearColor(0.30f, 0.55f, 0.65f, 1.0f);
  238.         glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  239.  
  240.         for (unsigned int i = 0; i < plane.num_meshes; ++i)
  241.         {
  242.             glBindTexture(GL_TEXTURE_2D, plane.mesh_list[i].tex_handle); // Bind texture for the current mesh
  243.  
  244.             glBindVertexArray(plane.mesh_list[i].VAO);
  245.             glDrawElements(GL_TRIANGLES, (GLsizei)plane.mesh_list[i].vert_indices.size(), GL_UNSIGNED_INT, 0);
  246.             glBindVertexArray(0);
  247.         }
  248.         glfwSwapBuffers(window);
  249.         glfwPollEvents();
  250.     }
  251.     // (11) Exit the Application
  252.     // -------------------------
  253.     glDeleteProgram(shader.ID);
  254.     glfwTerminate();
  255.     exit(EXIT_SUCCESS);
  256. }

Source code: C++... shader_configure.h & load_model_meshes.h

File shader_configure.h and file load_model_meshes.h can be found here: OpenGL Tutorial 5 (Quick Start) – Model Loading – Assimp Blender & Lighting



Source code: GLSL... shader_glsl.vert (vertex shader)

File: main.cpp
  1. #version 420 core
  2.  
  3. layout (location = 0) in vec3 aPos; // Attribute data: vertex(s) X, Y, Z position received via VBO on the CPU side
  4. layout (location = 1) in vec3 aNormal;
  5. layout (location = 2) in vec2 aTexCoord;
  6.  
  7. out vec3 vertex_normal;
  8. out vec2 texture_coordinates;
  9. out vec3 vert_pos_transformed; // Transformed model vertex position passed to fragment shader for lighting
  10.  
  11. out float aPos_dist_from_ray_intersect;
  12.  
  13. uniform mat4 sphere_trans_mat;
  14. uniform vec3 ray_intersect_point; // Where the ray hits the sphere
  15.  
  16. uniform mat4 view;
  17. uniform mat4 projection;
  18.  
  19. //void main()
  20. //{
  21. //    texture_coordinates = aTexCoord;
  22. //    vert_pos_transformed = aPos;
  23. //    vertex_normal = aNormal;
  24. //
  25. //    aPos_dist_from_ray_intersect = abs(length(vert_pos_transformed - ray_intersect_point));
  26. //
  27. //    gl_Position = projection * view * vec4(aPos, 1.0);
  28. //}
  29.  
  30. void main()
  31. {
  32.     texture_coordinates = aTexCoord;
  33.     vert_pos_transformed = vec3(sphere_trans_mat * vec4(aPos, 1.0));
  34.  
  35.     mat3 normal_matrix = transpose(inverse(mat3(sphere_trans_mat)));
  36.     vertex_normal = normal_matrix * aNormal;
  37.  
  38.     if (length(vertex_normal) > 0)
  39.         vertex_normal = normalize(vertex_normal);
  40.  
  41.     aPos_dist_from_ray_intersect = abs(length(vert_pos_transformed - ray_intersect_point));
  42.  
  43.     gl_Position = projection * view * sphere_trans_mat * vec4(aPos, 1.0);
  44. }


Source code: GLSL... shader_glsl.frag (fragment shader)

File: main.cpp
  1. #version 420 core
  2.  
  3. out vec4 fragment_colour;
  4.  
  5. // Must be the exact same name as declared in the vertex shader
  6. // ------------------------------------------------------------
  7. in vec3 vert_pos_transformed; // Transformed vertex position coordinates received as interpolated
  8. in vec3 vertex_normal;
  9. in vec2 texture_coordinates;
  10. in float aPos_dist_from_ray_intersect;
  11.  
  12. uniform sampler2D image;
  13. uniform vec3 camera_position; // camera_position is set in main() on the CPU side
  14.  
  15. void main()
  16. {
  17.     vec3 view_direction = normalize(camera_position - vert_pos_transformed);
  18.  
  19.     vec3 light_position = vec3(0.0, 20.0, 0.0); // Light position in 3D space
  20.     vec3 light_direction = normalize(vec3(light_position - vert_pos_transformed));
  21.  
  22.     vec4 image_colour = texture(image, texture_coordinates);
  23.  
  24.     float ambient_factor = 0.95; // Intensity multiplier
  25.     vec4 ambient_result = vec4(ambient_factor * image_colour.rgb, 1.0);
  26.  
  27.     float diffuse_factor = 0.75;
  28.     float diffuse_angle = max(dot(light_direction, vertex_normal), -0.1); // [-1.0 to 0] Results in darker lighting past 90 degrees
  29.     vec4 diffuse_result =  vec4(diffuse_factor * diffuse_angle * image_colour.rgb, 1.0);
  30.  
  31.     vec3 specular_colour = vec3(0.5, 0.5, 0.5);
  32.     vec3 reflect_direction = normalize(reflect(-light_direction, vertex_normal)); // Notice the light direction is negated here
  33.     float specular_strength = pow(max(dot(view_direction, reflect_direction), 0), 32);
  34.     vec4 specular_result = vec4(specular_colour * specular_strength, 1.0);
  35.  
  36.     fragment_colour = ambient_result + diffuse_result + specular_result;
  37.  
  38.     if (aPos_dist_from_ray_intersect > 0)
  39.         fragment_colour.g += 0.1 / aPos_dist_from_ray_intersect;
  40. //        fragment_colour.a -= 0.5 / aPos_dist_from_ray_intersect;
  41. }