From 64eb5b1a7aac612b350828efffa4925c8781ec8d Mon Sep 17 00:00:00 2001 From: Alexey Karnachev Date: Sat, 30 Dec 2023 15:06:54 +0400 Subject: [PATCH] Add others/gizmo example --- examples/Makefile | 3 +- examples/Makefile.Web | 6 +- examples/README.md | 1 + examples/others/gizmo.c | 113 +++++++ examples/others/gizmo.h | 656 ++++++++++++++++++++++++++++++++++++++ examples/others/gizmo.png | Bin 0 -> 35811 bytes 6 files changed, 777 insertions(+), 2 deletions(-) create mode 100644 examples/others/gizmo.c create mode 100644 examples/others/gizmo.h create mode 100644 examples/others/gizmo.png diff --git a/examples/Makefile b/examples/Makefile index 235deda28..01458c6bc 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -585,7 +585,8 @@ OTHERS = \ others/raylib_opengl_interop \ others/raymath_vector_angle \ others/rlgl_compute_shader \ - others/rlgl_standalone + others/rlgl_standalone \ + others/gizmo CURRENT_MAKEFILE = $(lastword $(MAKEFILE_LIST)) diff --git a/examples/Makefile.Web b/examples/Makefile.Web index 8bec06452..d7dbffcb5 100644 --- a/examples/Makefile.Web +++ b/examples/Makefile.Web @@ -491,7 +491,8 @@ OTHERS = \ others/raylib_opengl_interop \ others/raymath_vector_angle \ others/rlgl_compute_shader \ - others/rlgl_standalone + others/rlgl_standalone \ + others/gizmo CURRENT_MAKEFILE = $(lastword $(MAKEFILE_LIST)) @@ -1099,6 +1100,9 @@ others/rlgl_compute_shader: others/rlgl_standalone: $(info Skipping_others_rlgl_standalone) +others/gizmo: others/gizmo.c + $(CC) -o $@$(EXT) $< $(CFLAGS) $(INCLUDE_PATHS) $(LDFLAGS) $(LDLIBS) -D$(PLATFORM) + # Clean everything clean: ifeq ($(PLATFORM),PLATFORM_DESKTOP) diff --git a/examples/README.md b/examples/README.md index 0fed8acd0..dcbdd6e8a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -200,6 +200,7 @@ Examples showing raylib misc functionality that does not fit in other categories | 124 | [easings_testbed](others/easings_testbed.c) | easings_testbed | ⭐️⭐️⭐️☆ | 3.0 | 3.0 | [Juan Miguel López](https://github.com/flashback-fx) | | 125 | [raylib_opengl_interop](others/raylib_opengl_interop.c) | raylib_opengl_interop | ⭐️⭐️⭐️⭐️ | **4.0** | **4.0** | [Stephan Soller](https://github.com/arkanis) | | 126 | [embedded_files_loading](others/embedded_files_loading.c) | embedded_files_loading | ⭐️⭐️☆☆ | 3.5 | 3.5 | [Kristian Holmgren](https://github.com/defutura) | +| 127 | [gizmo](others/gizmo.c) | gizmo | ⭐️⭐️☆☆ | 5.0 | 5.0 | [Alexey Karnachev](https://github.com/alexeykarnachev) | As always contributions are welcome, feel free to send new examples! Here it is an [examples template](examples_template.c) to start with! diff --git a/examples/others/gizmo.c b/examples/others/gizmo.c new file mode 100644 index 000000000..13729b6b0 --- /dev/null +++ b/examples/others/gizmo.c @@ -0,0 +1,113 @@ +#include "raylib.h" +#include "rcamera.h" + +#define RAYGIZMO_IMPLEMENTATION +#include "gizmo.h" +#undef RAYGIZMO_IMPLEMENTATION + +void updateCamera(Camera3D* camera) { + float rot_speed = 0.003f; + float move_speed = 0.01f; + float zoom_speed = 1.0f; + + bool is_middle = IsMouseButtonDown(2); + bool is_shift = IsKeyDown(KEY_LEFT_SHIFT); + Vector2 mouse_delta = GetMouseDelta(); + + if (is_middle && is_shift) { + CameraMoveRight(camera, -move_speed * mouse_delta.x, true); + + Vector3 right = GetCameraRight(camera); + Vector3 up = Vector3CrossProduct( + Vector3Subtract(camera->position, camera->target), right + ); + up = Vector3Scale(Vector3Normalize(up), move_speed * mouse_delta.y); + camera->position = Vector3Add(camera->position, up); + camera->target = Vector3Add(camera->target, up); + } else if (is_middle) { + CameraYaw(camera, -rot_speed * mouse_delta.x, true); + CameraPitch(camera, rot_speed * mouse_delta.y, true, true, false); + } + + CameraMoveToTarget(camera, -GetMouseWheelMove() * zoom_speed); +} + +int main(void) { + const int screen_width = 800; + const int screen_height = 450; + InitWindow(screen_width, screen_height, "Gizmo"); + SetTargetFPS(60); + + Camera3D camera; + camera.fovy = 45.0f; + camera.target = (Vector3){0.0f, 0.0f, 0.0f}; + camera.position = (Vector3){5.0f, 5.0f, 5.0f}; + camera.up = (Vector3){0.0f, 1.0f, 0.0f}; + camera.projection = CAMERA_PERSPECTIVE; + + Model model = LoadModelFromMesh(GenMeshTorus(0.3, 1.5, 16.0, 16.0)); + + LoadGizmo(); + + while (!WindowShouldClose()) { + updateCamera(&camera); + + BeginDrawing(); + { + ClearBackground(DARKGRAY); + rlEnableDepthTest(); + + BeginMode3D(camera); + { + // Draw main model + DrawModel(model, (Vector3){0.0, 0.0, 0.0}, 1.0, PURPLE); + + // Draw coordinates grid + rlSetLineWidth(1.0); + DrawGrid(100.0, 1.0); + + // Draw coordinate x, y and z axis + rlSetLineWidth(2.0); + DrawLine3D( + (Vector3){-50.0f, 0.0f, 0.0f}, + (Vector3){50.0f, 0.0f, 0.0f}, + RED + ); + DrawLine3D( + (Vector3){0.0f, -50.0f, 0.0f}, + (Vector3){0.0f, 50.0f, 0.0f}, + GREEN + ); + DrawLine3D( + (Vector3){0.0f, 0.0f, -50.0f}, + (Vector3){0.0f, 0.0f, 50.0f}, + DARKBLUE + ); + } + EndMode3D(); + + // Immediately update and draw gizmo + Vector3 position = { + model.transform.m12, model.transform.m13, model.transform.m14}; + Matrix transform = UpdateGizmo(camera, position); + + // Apply gizmo-produced transformation to the model + model.transform = MatrixMultiply(model.transform, transform); + + // Draw camera controll keys + DrawRectangle(0, 0, 280, 90, RAYWHITE); + DrawText("CAMERA:", 5, 5, 20, RED); + DrawText(" zoom: wheel", 5, 25, 20, RED); + DrawText(" rotate: mmb", 5, 45, 20, RED); + DrawText(" translate: shift + mmb", 5, 65, 20, RED); + } + EndDrawing(); + } + + UnloadGizmo(); + UnloadModel(model); + CloseWindow(); + + return 0; +} + diff --git a/examples/others/gizmo.h b/examples/others/gizmo.h new file mode 100644 index 000000000..c7348f67b --- /dev/null +++ b/examples/others/gizmo.h @@ -0,0 +1,656 @@ +#include +#include + +void LoadGizmo(); +void UnloadGizmo(); +Matrix UpdateGizmo(Camera3D camera, Vector3 position); + +#ifdef RAYGIZMO_IMPLEMENTATION + +#include "raymath.h" +#include +#include +#include +#include + +// This will be multiplied by the distance from the camera to the gizmo, +// Which keeps the screen-space gizmo size constant. +#define GIZMO_SIZE 0.12f + +// Line drawing thicknesses +#define GIZMO_HANDLE_DRAW_THICKNESS 5.0f +#define GIZMO_ACTIVE_AXIS_DRAW_THICKNESS 2.0f + +// These sizes are relative to the gizmo radius +#define GIZMO_AXIS_HANDLE_LENGTH 1.2; +#define GIZMO_AXIS_HANDLE_TIP_LENGTH 0.3; +#define GIZMO_AXIS_HANDLE_TIP_RADIUS 0.1; +#define GIZMO_PLANE_HANDLE_OFFSET 0.4; +#define GIZMO_PLANE_HANDLE_SIZE 0.2; + +#define X_AXIS \ + (Vector3) { 1.0, 0.0, 0.0 } +#define Y_AXIS \ + (Vector3) { 0.0, 1.0, 0.0 } +#define Z_AXIS \ + (Vector3) { 0.0, 0.0, 1.0 } + +#define MASK_FRAMEBUFFER_WIDTH 500.0 +#define MASK_FRAMEBUFFER_HEIGHT 500.0 + +#define swap(x, y) \ + do { \ + unsigned char \ + swap_temp[sizeof(x) == sizeof(y) ? (signed)sizeof(x) : -1]; \ + memcpy(swap_temp, &y, sizeof(x)); \ + memcpy(&y, &x, sizeof(x)); \ + memcpy(&x, swap_temp, sizeof(x)); \ + } while (0) + +#define id_to_red_color(id) \ + (Color) { id, 0, 0, 0 } + +static const char* SHADER_COLOR_VERT = "\ +#version 330\n\ +in vec3 vertexPosition; \ +in vec4 vertexColor; \ +out vec4 fragColor; \ +out vec3 fragPosition; \ +uniform mat4 mvp; \ +void main() { \ + fragColor = vertexColor; \ + fragPosition = vertexPosition; \ + gl_Position = mvp * vec4(vertexPosition, 1.0); \ +} \ +"; + +static const char* SHADER_ROT_HANDLE_COLOR_FRAG = "\ +#version 330\n\ +in vec4 fragColor; \ +in vec3 fragPosition; \ +uniform vec3 cameraPosition; \ +uniform vec3 gizmoPosition; \ +out vec4 finalColor; \ +void main() { \ + vec3 r = normalize(fragPosition - gizmoPosition); \ + vec3 c = normalize(fragPosition - cameraPosition); \ + if (dot(r, c) > 0.1) discard; \ + finalColor = fragColor; \ +} \ +"; + +static Shader SHADER_ROT_HANDLE_COLOR; +static int SHADER_ROT_HANDLE_CAMERA_POS_LOC; +static int SHADER_ROT_HANDLE_GIZMO_POS_LOC; + +static unsigned int MASK_FRAMEBUFFER; +static unsigned int MASK_TEXTURE; + +static bool IS_GIZMO_LOADED; +static Vector3 GIZMO_CURRENT_AXIS; + +typedef enum HandleId { + HANDLE_X, + + ROT_HANDLE_X, + AXIS_HANDLE_X, + PLANE_HANDLE_X, + + HANDLE_Y, + + ROT_HANDLE_Y, + AXIS_HANDLE_Y, + PLANE_HANDLE_Y, + + HANDLE_Z, + + ROT_HANDLE_Z, + AXIS_HANDLE_Z, + PLANE_HANDLE_Z +} HandleId; + +typedef enum GizmoState { + GIZMO_COLD, + + GIZMO_HOT, + + GIZMO_HOT_ROT, + GIZMO_HOT_AXIS, + GIZMO_HOT_PLANE, + + GIZMO_ACTIVE, + + GIZMO_ACTIVE_ROT, + GIZMO_ACTIVE_AXIS, + GIZMO_ACTIVE_PLANE, +} GizmoState; + +typedef struct Handle { + Vector3 position; + Vector3 axis; + Color color; + float dist_to_cam; +} Handle; + +typedef struct HandleColors { + Color x; + Color y; + Color z; +} HandleColors; + +typedef struct Handles { + Handle arr[3]; +} Handles; + +static GizmoState GIZMO_STATE; + +bool check_if_mouse_moved() { + Vector2 mouse_delta = GetMouseDelta(); + return (fabs(mouse_delta.x) + fabs(mouse_delta.y)) > EPSILON; +} + +static float vector2_get_angle(Vector2 v1, Vector2 v2) { + Vector2 v1_norm = Vector2Normalize(v1); + Vector2 v2_norm = Vector2Normalize(v2); + float dot = Vector2DotProduct(v1_norm, v2_norm); + if (1.0 - fabs(dot) < EPSILON) return 0.0; + + float angle = acos(dot); + float z = v1.x * v2.y - v1.y * v2.x; + + if (fabs(z) < EPSILON) return 0.0; + else if (z > 0) return angle; + else return -angle; +} + +static int isect_line_plane( + Vector3* out_p, + Vector3 line_p0, + Vector3 line_p1, + Vector3 plane_p, + Vector3 plane_normal +) { + Vector3 u = Vector3Subtract(line_p1, line_p0); + float dot = Vector3DotProduct(plane_normal, u); + if (fabs(dot) <= EPSILON) { + return 0; + } + + Vector3 w = Vector3Subtract(line_p0, plane_p); + float k = -Vector3DotProduct(plane_normal, w) / dot; + u = Vector3Scale(u, k); + *out_p = Vector3Add(line_p0, u); + return 1; +} + +static int get_two_vecs_nearest_point( + Vector3* vec0_out_nearest_point, + Vector3 vec0_p0, + Vector3 vec0_p1, + Vector3 vec1_p0, + Vector3 vec1_p1 +) { + Vector3 vec0 = Vector3Subtract(vec0_p1, vec0_p0); + Vector3 vec1 = Vector3Subtract(vec1_p1, vec1_p0); + Vector3 plane_vec = Vector3CrossProduct(vec0, vec1); + plane_vec = Vector3Normalize(plane_vec); + Vector3 plane_normal = Vector3CrossProduct(vec0, plane_vec); + plane_normal = Vector3Normalize(plane_normal); + + int is_isect = isect_line_plane( + vec0_out_nearest_point, vec1_p0, vec1_p1, vec0_p0, plane_normal + ); + + return is_isect; +} + +static RayCollision get_ray_plane_collision( + Ray ray, Vector3 plane_point, Vector3 plane_normal +) { + RayCollision collision = {0}; + + // Calculate the parameter t + float denominator = ray.direction.x * plane_normal.x + + ray.direction.y * plane_normal.y + + ray.direction.z * plane_normal.z; + + if (denominator == 0) { + // Ray is parallel to the plane, no collision + return collision; + } + + float t = ((plane_point.x - ray.position.x) * plane_normal.x + + (plane_point.y - ray.position.y) * plane_normal.y + + (plane_point.z - ray.position.z) * plane_normal.z) + / denominator; + + if (t < 0) { + // Intersection point is behind the ray's starting point, no collision + return collision; + } + + // Calculate the collision point + collision.point.x = ray.position.x + t * ray.direction.x; + collision.point.y = ray.position.y + t * ray.direction.y; + collision.point.z = ray.position.z + t * ray.direction.z; + collision.hit = true; + + return collision; +} + +static Handles get_sorted_handles(Handle h0, Handle h1, Handle h2) { + if (h0.dist_to_cam < h1.dist_to_cam) swap(h0, h1); + if (h1.dist_to_cam < h2.dist_to_cam) swap(h1, h2); + if (h0.dist_to_cam < h1.dist_to_cam) swap(h0, h1); + + Handles handles = {.arr = {h0, h1, h2}}; + return handles; +} + +static HandleColors get_handle_colors(GizmoState hot_state) { + bool is_hot = GIZMO_STATE == hot_state || GIZMO_STATE == hot_state + 4; + Color x = is_hot && GIZMO_CURRENT_AXIS.x == 1.0 ? WHITE : RED; + Color y = is_hot && GIZMO_CURRENT_AXIS.y == 1.0 ? WHITE : GREEN; + Color z = is_hot && GIZMO_CURRENT_AXIS.z == 1.0 ? WHITE : BLUE; + HandleColors colors = {x, y, z}; + return colors; +} + +static void draw_axis_handles( + Camera3D camera, + Vector3 position, + float gizmo_radius, + Color color_x, + Color color_y, + Color color_z +) { + float length = gizmo_radius * GIZMO_AXIS_HANDLE_LENGTH; + float tip_length = gizmo_radius * GIZMO_AXIS_HANDLE_TIP_LENGTH; + float tip_radius = gizmo_radius * GIZMO_AXIS_HANDLE_TIP_RADIUS; + + Vector3 px = Vector3Add(position, Vector3Scale(X_AXIS, length)); + Vector3 py = Vector3Add(position, Vector3Scale(Y_AXIS, length)); + Vector3 pz = Vector3Add(position, Vector3Scale(Z_AXIS, length)); + + Handle hx = {px, X_AXIS, color_x, Vector3DistanceSqr(px, camera.position)}; + Handle hy = {py, Y_AXIS, color_y, Vector3DistanceSqr(py, camera.position)}; + Handle hz = {pz, Z_AXIS, color_z, Vector3DistanceSqr(pz, camera.position)}; + + Handles handles = get_sorted_handles(hx, hy, hz); + + for (int i = 0; i < 3; ++i) { + Handle* h = &handles.arr[i]; + Vector3 tip_end = Vector3Add( + h->position, Vector3Scale(h->axis, tip_length) + ); + DrawLine3D(position, h->position, h->color); + DrawCylinderEx(h->position, tip_end, tip_radius, 0.0, 16, h->color); + } +} + +static void draw_plane_handles( + Camera3D camera, + Vector3 position, + float gizmo_radius, + Color color_x, + Color color_y, + Color color_z +) { + float offset = gizmo_radius * GIZMO_PLANE_HANDLE_OFFSET; + float size = gizmo_radius * GIZMO_PLANE_HANDLE_SIZE; + + Vector3 px = Vector3Add(position, (Vector3){0.0, offset, offset}); + Vector3 py = Vector3Add(position, (Vector3){offset, 0.0, offset}); + Vector3 pz = Vector3Add(position, (Vector3){offset, offset, 0.0}); + + Handle hx = {px, Z_AXIS, color_x, Vector3DistanceSqr(px, camera.position)}; + Handle hy = {py, Y_AXIS, color_y, Vector3DistanceSqr(py, camera.position)}; + Handle hz = {pz, X_AXIS, color_z, Vector3DistanceSqr(pz, camera.position)}; + + Handles handles = get_sorted_handles(hx, hy, hz); + + rlDisableBackfaceCulling(); + for (int i = 0; i < 3; ++i) { + Handle* h = &handles.arr[i]; + rlPushMatrix(); + { + rlTranslatef(h->position.x, h->position.y, h->position.z); + rlRotatef(90.0, h->axis.x, h->axis.y, h->axis.z); + DrawPlane( + Vector3Zero(), Vector2Scale(Vector2One(), size), h->color + ); + } + rlPopMatrix(); + } +} + +static void draw_rot_handles( + Camera3D camera, + Vector3 position, + float gizmo_radius, + Color color_x, + Color color_y, + Color color_z +) { + BeginShaderMode(SHADER_ROT_HANDLE_COLOR); + { + SetShaderValue( + SHADER_ROT_HANDLE_COLOR, + SHADER_ROT_HANDLE_CAMERA_POS_LOC, + &camera.position, + SHADER_UNIFORM_VEC3 + ); + SetShaderValue( + SHADER_ROT_HANDLE_COLOR, + SHADER_ROT_HANDLE_GIZMO_POS_LOC, + &position, + SHADER_UNIFORM_VEC3 + ); + DrawCircle3D(position, gizmo_radius, Y_AXIS, 90.0, color_x); + DrawCircle3D(position, gizmo_radius, X_AXIS, 90.0, color_y); + DrawCircle3D(position, gizmo_radius, X_AXIS, 0.0, color_z); + } + EndShaderMode(); +} + +static void draw_gizmo( + Camera3D camera, + Vector3 position, + Color rot_handle_color_x, + Color rot_handle_color_y, + Color rot_handle_color_z, + Color axis_handle_color_x, + Color axis_handle_color_y, + Color axis_handle_color_z, + Color plane_handle_color_x, + Color plane_handle_color_y, + Color plane_handle_color_z +) { + float radius = GIZMO_SIZE * Vector3Distance(camera.position, position); + + // Draw gizmo's handle elements + BeginMode3D(camera); + { + rlSetLineWidth(GIZMO_HANDLE_DRAW_THICKNESS); + rlDisableDepthTest(); + + draw_plane_handles( + camera, + position, + radius, + plane_handle_color_x, + plane_handle_color_y, + plane_handle_color_z + ); + + draw_rot_handles( + camera, + position, + radius, + rot_handle_color_x, + rot_handle_color_y, + rot_handle_color_z + ); + + draw_axis_handles( + camera, + position, + radius, + axis_handle_color_x, + axis_handle_color_y, + axis_handle_color_z + ); + } + EndMode3D(); + + // Draw long white line which represents current active axis + if (GIZMO_STATE == GIZMO_ACTIVE_ROT || GIZMO_STATE == GIZMO_ACTIVE_AXIS) { + BeginMode3D(camera); + { + rlSetLineWidth(GIZMO_ACTIVE_AXIS_DRAW_THICKNESS); + Vector3 half_axis_line = Vector3Scale(GIZMO_CURRENT_AXIS, 1000.0); + DrawLine3D( + Vector3Subtract(position, half_axis_line), + Vector3Add(position, half_axis_line), + WHITE + ); + } + EndMode3D(); + } + + // Draw white line from the gizmo's center to the mouse cursor when rotating + if (GIZMO_STATE == GIZMO_ACTIVE_ROT) { + rlSetLineWidth(GIZMO_ACTIVE_AXIS_DRAW_THICKNESS); + DrawLineV( + GetWorldToScreen(position, camera), GetMousePosition(), WHITE + ); + } +} + +static unsigned char get_gizmo_mask_pixel(Camera3D camera, Vector3 position) { + // ------------------------------------------------------------------- + // Draw gizmo into a separate framebuffer for the mouse pixel-picking + rlEnableFramebuffer(MASK_FRAMEBUFFER); + rlViewport(0, 0, MASK_FRAMEBUFFER_WIDTH, MASK_FRAMEBUFFER_HEIGHT); + rlClearScreenBuffers(); + rlDisableColorBlend(); + + draw_gizmo( + camera, + position, + id_to_red_color(ROT_HANDLE_X), + id_to_red_color(ROT_HANDLE_Y), + id_to_red_color(ROT_HANDLE_Z), + id_to_red_color(AXIS_HANDLE_X), + id_to_red_color(AXIS_HANDLE_Y), + id_to_red_color(AXIS_HANDLE_Z), + id_to_red_color(PLANE_HANDLE_X), + id_to_red_color(PLANE_HANDLE_Y), + id_to_red_color(PLANE_HANDLE_Z) + ); + + rlDisableFramebuffer(); + rlEnableColorBlend(); + rlViewport(0, 0, GetScreenWidth(), GetScreenHeight()); + + // ------------------------------------------------------------------- + // Pick the pixel under the mouse cursor + Vector2 mouse_position = GetMousePosition(); + unsigned char* pixels = (unsigned char*)rlReadTexturePixels( + MASK_TEXTURE, + MASK_FRAMEBUFFER_WIDTH, + MASK_FRAMEBUFFER_HEIGHT, + RL_PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 + ); + + float x_ratio = Clamp(mouse_position.x / (float)GetScreenWidth(), 0.0, 1.0); + float y_ratio = Clamp( + 1.0 - (mouse_position.y / (float)GetScreenHeight()), 0.0, 1.0 + ); + int x = (int)(MASK_FRAMEBUFFER_WIDTH * x_ratio); + int y = (int)(MASK_FRAMEBUFFER_HEIGHT * y_ratio); + int idx = 4 * (y * MASK_FRAMEBUFFER_WIDTH + x); + unsigned char mask_val = pixels[idx]; + + free(pixels); + return mask_val; +} + +Matrix update_gizmo_rot(Camera3D camera, Vector3 position) { + if (!check_if_mouse_moved()) return MatrixIdentity(); + + Vector2 rot_center = GetWorldToScreen(position, camera); + Vector2 p1 = Vector2Subtract(GetMousePosition(), rot_center); + Vector2 p0 = Vector2Subtract(p1, GetMouseDelta()); + float angle = vector2_get_angle(p1, p0); + + // If we look at gizmo from behind, we should flip the rotation + if (Vector3DotProduct(GIZMO_CURRENT_AXIS, position) + > Vector3DotProduct(GIZMO_CURRENT_AXIS, camera.position)) { + angle *= -1; + } + + // We rotate gizmo relative to it's own center, so we first translate it to + // the center of world coordinates, then rotate and then translate back + Matrix transform = MatrixMultiply( + MatrixMultiply( + MatrixTranslate(-position.x, -position.y, -position.z), + MatrixRotate(GIZMO_CURRENT_AXIS, angle) + ), + MatrixTranslate(position.x, position.y, position.z) + ); + + return transform; +} + +Matrix update_gizmo_axis(Camera3D camera, Vector3 position) { + if (!check_if_mouse_moved()) return MatrixIdentity(); + + Vector2 p = Vector2Add(GetWorldToScreen(position, camera), GetMouseDelta()); + Ray r = GetMouseRay(p, camera); + Vector3 isect_p = Vector3Zero(); + bool is_isect = get_two_vecs_nearest_point( + &isect_p, + camera.position, + Vector3Add(camera.position, r.direction), + position, + Vector3Add(position, GIZMO_CURRENT_AXIS) + ); + + if (!is_isect) + { + return MatrixIdentity(); + } + + Vector3 offset = Vector3Subtract(isect_p, position); + offset = Vector3Multiply(offset, GIZMO_CURRENT_AXIS); + Matrix transform = MatrixTranslate(offset.x, offset.y, offset.z); + + return transform; +} + +Matrix update_gizmo_plane(Camera3D camera, Vector3 position) { + if (!check_if_mouse_moved()) return MatrixIdentity(); + Vector2 p = Vector2Add(GetWorldToScreen(position, camera), GetMouseDelta()); + Ray r = GetMouseRay(p, camera); + RayCollision c = get_ray_plane_collision(r, position, GIZMO_CURRENT_AXIS); + Vector3 offset = Vector3Subtract(c.point, position); + Matrix transform = MatrixTranslate(offset.x, offset.y, offset.z); + + return transform; +} + +Matrix update_gizmo(Camera3D camera, Vector3 position, unsigned char mask_val) { + bool is_lmb_down = IsMouseButtonDown(0); + + if (!is_lmb_down) GIZMO_STATE = GIZMO_COLD; + + if (GIZMO_STATE < GIZMO_ACTIVE) { + if (mask_val < HANDLE_Y) GIZMO_CURRENT_AXIS = X_AXIS; + else if (mask_val < HANDLE_Z) GIZMO_CURRENT_AXIS = Y_AXIS; + else GIZMO_CURRENT_AXIS = Z_AXIS; + + if (mask_val % 4 == 1) + GIZMO_STATE = is_lmb_down ? GIZMO_ACTIVE_ROT : GIZMO_HOT_ROT; + else if (mask_val % 4 == 2) + GIZMO_STATE = is_lmb_down ? GIZMO_ACTIVE_AXIS : GIZMO_HOT_AXIS; + else if (mask_val % 4 == 3) + GIZMO_STATE = is_lmb_down ? GIZMO_ACTIVE_PLANE : GIZMO_HOT_PLANE; + } + + switch (GIZMO_STATE) { + case GIZMO_ACTIVE_ROT: return update_gizmo_rot(camera, position); + case GIZMO_ACTIVE_AXIS: return update_gizmo_axis(camera, position); + case GIZMO_ACTIVE_PLANE: return update_gizmo_plane(camera, position); + default: return MatrixIdentity(); + } +} + +void LoadGizmo() { + if (IS_GIZMO_LOADED) return; + + SHADER_ROT_HANDLE_COLOR = LoadShaderFromMemory( + SHADER_COLOR_VERT, SHADER_ROT_HANDLE_COLOR_FRAG + ); + SHADER_ROT_HANDLE_CAMERA_POS_LOC = GetShaderLocation( + SHADER_ROT_HANDLE_COLOR, "cameraPosition" + ); + SHADER_ROT_HANDLE_GIZMO_POS_LOC = GetShaderLocation( + SHADER_ROT_HANDLE_COLOR, "gizmoPosition" + ); + + MASK_FRAMEBUFFER = rlLoadFramebuffer( + MASK_FRAMEBUFFER_WIDTH, MASK_FRAMEBUFFER_HEIGHT + ); + if (!MASK_FRAMEBUFFER) { + TraceLog(LOG_ERROR, "Failed to create gizmo's mask framebuffer"); + exit(1); + } + rlEnableFramebuffer(MASK_FRAMEBUFFER); + + MASK_TEXTURE = rlLoadTexture( + NULL, + MASK_FRAMEBUFFER_WIDTH, + MASK_FRAMEBUFFER_HEIGHT, + RL_PIXELFORMAT_UNCOMPRESSED_R32G32B32A32, + 1 + ); + rlActiveDrawBuffers(1); + rlFramebufferAttach( + MASK_FRAMEBUFFER, + MASK_TEXTURE, + RL_ATTACHMENT_COLOR_CHANNEL0, + RL_ATTACHMENT_TEXTURE2D, + 0 + ); + if (!rlFramebufferComplete(MASK_FRAMEBUFFER)) { + TraceLog(LOG_ERROR, "Gizmo's framebuffer is not complete"); + exit(1); + } + + IS_GIZMO_LOADED = true; +} + +void UnloadGizmo() { + if (!IS_GIZMO_LOADED) return; + + UnloadShader(SHADER_ROT_HANDLE_COLOR); + rlUnloadFramebuffer(MASK_FRAMEBUFFER); + rlUnloadTexture(MASK_TEXTURE); + IS_GIZMO_LOADED = false; +} + +Matrix UpdateGizmo(Camera3D camera, Vector3 position) { + if (!IS_GIZMO_LOADED) { + TraceLog(LOG_ERROR, "Gizmo must be loaded before the update"); + exit(1); + } + + unsigned char mask_val = get_gizmo_mask_pixel(camera, position); + Matrix transform = update_gizmo(camera, position, mask_val); + + // ------------------------------------------------------------------- + // Draw gizmo + HandleColors rot_handle_colors = get_handle_colors(GIZMO_HOT_ROT); + HandleColors axis_handle_colors = get_handle_colors(GIZMO_HOT_AXIS); + HandleColors plane_handle_colors = get_handle_colors(GIZMO_HOT_PLANE); + + draw_gizmo( + camera, + position, + rot_handle_colors.x, + rot_handle_colors.y, + rot_handle_colors.z, + axis_handle_colors.x, + axis_handle_colors.y, + axis_handle_colors.z, + plane_handle_colors.x, + plane_handle_colors.y, + plane_handle_colors.z + ); + + return transform; +} + +#endif + diff --git a/examples/others/gizmo.png b/examples/others/gizmo.png new file mode 100644 index 0000000000000000000000000000000000000000..b2346d4ef24c52fc9337c491c33d0081cd46c449 GIT binary patch literal 35811 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

^UO@|Q-ZuR-tXT-#^bIrVz^@rb; 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