From 97101d1003b182af354530c1447a1ee67af47e8c Mon Sep 17 00:00:00 2001 From: arvyy Date: Tue, 27 Aug 2019 14:05:28 +0300 Subject: [PATCH] Add screen->world and world->screen functions for 2D; add extended camera2D example (#947) --- examples/core/core_2d_camera_ext.c | 284 +++++++++++++++++++++++++++++ src/core.c | 59 +++--- src/raylib.h | 3 + 3 files changed, 324 insertions(+), 22 deletions(-) create mode 100644 examples/core/core_2d_camera_ext.c diff --git a/examples/core/core_2d_camera_ext.c b/examples/core/core_2d_camera_ext.c new file mode 100644 index 000000000..330e39ef9 --- /dev/null +++ b/examples/core/core_2d_camera_ext.c @@ -0,0 +1,284 @@ +/******************************************************************************************* +* +* raylib [core] example - 2d camera extended +* +* This example has been created using raylib 1.5 (www.raylib.com) +* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* +* Copyright (c) 2016 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "raylib.h" +#include "raymath.h" + +#define G 400 +#define PLAYER_JUMP_SPD 350.f +#define PLAYER_HOR_SPD 200.f + +typedef struct Player { + Vector2 pos; + float vel; + int canJump; +} Player; + +typedef struct EnvItem { + Rectangle rect; + int blocking; + Color color; +} EnvItem; + +void updateCameraCenter( + float delta, + Camera2D *camera, + Player *player, + EnvItem *envItems, + int envItemsLength, + int width, int height +) { + camera->offset = (Vector2){ width/2, height/2 }; + camera->target = player->pos; +} + +void updateCameraCenterInsideMap( + float delta, + Camera2D *camera, + Player *player, + EnvItem *envItems, + int envItemsLength, + int width, int height +) { + camera->target = player->pos; + camera->offset = (Vector2){ width/2, height/2 }; + float minX = 1000, minY = 1000, maxX = -1000, maxY = -1000; + for (int i = 0; i < envItemsLength; i++) { + EnvItem *ei = envItems + i; + minX = fminf(ei->rect.x, minX); + maxX = fmaxf(ei->rect.x + ei->rect.width, maxX); + minY = fminf(ei->rect.y, minY); + maxY = fmaxf(ei->rect.y + ei->rect.height, maxY); + } + Vector2 max = GetWorldToScreen2D((Vector2){ maxX, maxY }, *camera); + Vector2 min = GetWorldToScreen2D((Vector2){ minX, minY }, *camera); + if (max.x < width) { + camera->offset.x = width - (max.x - width/2); + } + if (max.y < height) { + camera->offset.y = height - (max.y - height/2); + } + if (min.x > 0) { + camera->offset.x = width/2 - min.x; + } + if (min.y > 0) { + camera->offset.y = height/2- min.y; + } +} + +void updateCameraCenterSmoothFollow( + float delta, + Camera2D *camera, + Player *player, + EnvItem *envItems, + int envItemsLength, + int width, int height +) { + static float minSpeed = 30; + static float minEffectLength = 10; + static float fractionSpeed = 0.8f; + camera->offset = (Vector2){ width/2, height/2 }; + Vector2 diff = Vector2Subtract(player->pos, camera->target); + float length = Vector2Length(diff); + if (length > minEffectLength) { + float speed = fmaxf(fractionSpeed * length, minSpeed); + camera->target = Vector2Add(camera->target, Vector2Scale(diff, speed*delta/length)); + } +} + +void updateCameraEvenOutOnLanding( + float delta, + Camera2D *camera, + Player *player, + EnvItem *envItems, + int envItemsLength, + int width, int height +) { + static float evenOutSpeed = 700; + static int eveningOut = false; + static float evenOutTarget; + camera->offset = (Vector2){ width/2, height/2 }; + camera->target.x = player->pos.x; + if (eveningOut) { + if (evenOutTarget > camera->target.y) { + camera->target.y += evenOutSpeed * delta; + if (camera->target.y > evenOutTarget) { + camera->target.y = evenOutTarget; + eveningOut = 0; + } + } else { + camera->target.y -= evenOutSpeed * delta; + if (camera->target.y < evenOutTarget) { + camera->target.y = evenOutTarget; + eveningOut = 0; + } + } + } else { + if (player->canJump && + player->vel == 0 && + player->pos.y != camera->target.y + ) { + eveningOut = 1; + evenOutTarget = player->pos.y; + } + } +} + +void updateCameraPlayerBoundsPush( + float delta, + Camera2D *camera, + Player *player, + EnvItem *envItems, + int envItemsLength, + int width, int height +) { + static Vector2 bbox = { 0.2f, 0.2f }; + + Vector2 bboxWorldMin = GetScreenToWorld2D((Vector2){ (1 - bbox.x) * 0.5 * width, (1 - bbox.y) * 0.5 * height }, *camera); + Vector2 bboxWorldMax = GetScreenToWorld2D((Vector2){ (1 + bbox.x) * 0.5 * width, (1 + bbox.y) * 0.5 * height }, *camera); + camera->offset = (Vector2){ (1 - bbox.x) * 0.5 * width, (1 - bbox.y) * 0.5 * height }; + + if (player->pos.x < bboxWorldMin.x) { + camera->target.x = player->pos.x; + } + if (player->pos.y < bboxWorldMin.y) { + camera->target.y = player->pos.y; + } + if (player->pos.x > bboxWorldMax.x) { + camera->target.x = bboxWorldMin.x + (player->pos.x - bboxWorldMax.x); + } + if (player->pos.y > bboxWorldMax.y) { + camera->target.y = bboxWorldMin.y + (player->pos.y - bboxWorldMax.y); + } +} + + +void updatePlayer(float delta, Player *player, EnvItem *envItems, int envItemsLength) { + if (IsKeyDown(KEY_LEFT)) player->pos.x -= PLAYER_HOR_SPD*delta; + if (IsKeyDown(KEY_RIGHT)) player->pos.x += PLAYER_HOR_SPD*delta; + if (IsKeyDown(KEY_SPACE) && player->canJump) { + player->vel = -PLAYER_JUMP_SPD; + player->canJump = 0; + } + + int hitObstacle = 0; + for (int i = 0; i < envItemsLength; i++) { + EnvItem *ei = envItems + i; + Vector2 *p = &(player->pos); + if (ei->blocking && + ei->rect.x <= p->x && + ei->rect.x + ei->rect.width >= p->x && + ei->rect.y >= p->y && + ei->rect.y < p->y + player->vel * delta) + { + hitObstacle = 1; + player->vel = 0.0f; + p->y = ei->rect.y; + } + } + if (!hitObstacle) { + player->pos.y += player->vel * delta; + player->vel += G * delta; + player->canJump = 0; + } else { + player->canJump = 1; + } +} + +void renderWorld(Player *player, EnvItem *envItems, int envItemsLength) { + for (int i = 0; i < envItemsLength; i++) { + DrawRectangleRec(envItems[i].rect, envItems[i].color); + } + Rectangle playerRect = { player->pos.x - 20, player->pos.y - 40, 40, 40 }; + DrawRectangleRec(playerRect, RED); +} + +int main(void) +{ + const int screenWidth = 800; + const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera"); + SetTargetFPS(60); + + Player player; + player.pos = (Vector2){ 400, 280 }; + player.vel = 0; + player.canJump = 0; + EnvItem envItems[] = { + {{ 0, 0, 1000, 400 }, 0, LIGHTGRAY }, + {{ 0, 400, 1000, 200 }, 1, GRAY }, + {{ 300, 200, 400, 10 }, 1, GRAY }, + {{ 250, 300, 100, 10 }, 1, GRAY }, + {{ 650, 300, 100, 10 }, 1, GRAY } + }; + int envItemsLength = sizeof(envItems) / sizeof (envItems[0]); + + Camera2D camera = { 0 }; + camera.target = player.pos; + camera.offset = (Vector2){ screenWidth/2, screenHeight/2 }; + camera.rotation = 0.0f; + camera.zoom = 1.0f; + + int cameraOption = 0; + void (*cameraUpdaters[])(float, Camera2D*, Player*, EnvItem*, int, int, int) = { + updateCameraCenter, + updateCameraCenterInsideMap, + updateCameraCenterSmoothFollow, + updateCameraEvenOutOnLanding, + updateCameraPlayerBoundsPush + }; + int cameraUpdatersLength = sizeof(cameraUpdaters) / sizeof(cameraUpdaters[0]); + char* cameraDescriptions[] = { + "Follow player center", + "Follow player center, but clamp to map edges", + "Follow player center; smoothed", + "Follow player center horizontally; updateplayer center vertically after landing", + "Player push camera on getting too close to screen edge" + }; + + while (!WindowShouldClose()) { + float delta = GetFrameTime(); + updatePlayer(delta, &player, envItems, envItemsLength); + + camera.zoom += ((float)GetMouseWheelMove()*0.05f); + if (camera.zoom > 3.0f) camera.zoom = 3.0f; + else if (camera.zoom < 0.25f) camera.zoom = 0.25f; + if (IsKeyPressed(KEY_R)) + { + camera.zoom = 1.0f; + } + + if (IsKeyPressed(KEY_C)) { + cameraOption = (cameraOption + 1) % cameraUpdatersLength; + } + cameraUpdaters[cameraOption](delta, &camera, &player, envItems, envItemsLength, screenWidth, screenHeight); + + BeginDrawing(); + ClearBackground(RAYWHITE); + + BeginMode2D(camera); + renderWorld(&player, envItems, envItemsLength); + EndMode2D(); + + DrawText("Controls:", 20, 20, 10, BLACK); + DrawText("- Right/Left to move", 40, 40, 10, DARKGRAY); + DrawText("- Space to jump", 40, 60, 10, DARKGRAY); + DrawText("- Mouse Wheel to Zoom in-out, R to reset zoom", 40, 80, 10, DARKGRAY); + DrawText("- C to change camera mode", 40, 100, 10, DARKGRAY); + DrawText("Current camera mode:", 20, 120, 10, BLACK); + DrawText(cameraDescriptions[cameraOption], 40, 140, 10, DARKGRAY); + EndDrawing(); + } + + CloseWindow(); // Close window and OpenGL context + + return 0; +} diff --git a/src/core.c b/src/core.c index c025b2dea..987f6e678 100644 --- a/src/core.c +++ b/src/core.c @@ -1250,28 +1250,7 @@ void BeginMode2D(Camera2D camera) rlLoadIdentity(); // Reset current matrix (MODELVIEW) rlMultMatrixf(MatrixToFloat(screenScaling)); // Apply screen scaling if required - // The camera in world-space is set by - // 1. Move it to target - // 2. Rotate by -rotation and scale by (1/zoom) - // When setting higher scale, it's more intuitive for the world to become bigger (= camera become smaller), - // not for the camera getting bigger, hence the invert. Same deal with rotation. - // 3. Move it by (-offset); - // Offset defines target transform relative to screen, but since we're effectively "moving" screen (camera) - // we need to do it into opposite direction (inverse transform) - - // Having camera transform in world-space, inverse of it gives the modelview transform. - // Since (A*B*C)' = C'*B'*A', the modelview is - // 1. Move to offset - // 2. Rotate and Scale - // 3. Move by -target - Matrix matOrigin = MatrixTranslate(-camera.target.x, -camera.target.y, 0.0f); - Matrix matRotation = MatrixRotate((Vector3){ 0.0f, 0.0f, 1.0f }, camera.rotation*DEG2RAD); - Matrix matScale = MatrixScale(camera.zoom, camera.zoom, 1.0f); - Matrix matTranslation = MatrixTranslate(camera.offset.x, camera.offset.y, 0.0f); - - Matrix matTransform = MatrixMultiply(MatrixMultiply(matOrigin, MatrixMultiply(matScale, matRotation)), matTranslation); - - rlMultMatrixf(MatrixToFloat(matTransform)); // Apply transformation to modelview + rlMultMatrixf(MatrixToFloat(GetCamera2DMatrix(camera))); // Apply transformation to modelview } // Ends 2D mode with custom camera @@ -1502,6 +1481,42 @@ Matrix GetCameraMatrix(Camera camera) return MatrixLookAt(camera.position, camera.target, camera.up); } + +Vector2 GetScreenToWorld2D(Vector2 position, Camera2D camera) { + Matrix m = MatrixInvert(GetCamera2DMatrix(camera)); + Vector3 transform = Vector3Transform((Vector3){position.x, position.y, 0}, m); + return (Vector2){transform.x, transform.y}; +} + +Vector2 GetWorldToScreen2D(Vector2 position, Camera2D camera) { + Matrix m = GetCamera2DMatrix(camera); + Vector3 transform = Vector3Transform((Vector3){position.x, position.y, 0}, m); + return (Vector2){transform.x, transform.y}; +} + +Matrix GetCamera2DMatrix(Camera2D camera) { + // The camera in world-space is set by + // 1. Move it to target + // 2. Rotate by -rotation and scale by (1/zoom) + // When setting higher scale, it's more intuitive for the world to become bigger (= camera become smaller), + // not for the camera getting bigger, hence the invert. Same deal with rotation. + // 3. Move it by (-offset); + // Offset defines target transform relative to screen, but since we're effectively "moving" screen (camera) + // we need to do it into opposite direction (inverse transform) + + // Having camera transform in world-space, inverse of it gives the modelview transform. + // Since (A*B*C)' = C'*B'*A', the modelview is + // 1. Move to offset + // 2. Rotate and Scale + // 3. Move by -target + Matrix matOrigin = MatrixTranslate(-camera.target.x, -camera.target.y, 0.0f); + Matrix matRotation = MatrixRotate((Vector3){ 0.0f, 0.0f, 1.0f }, camera.rotation*DEG2RAD); + Matrix matScale = MatrixScale(camera.zoom, camera.zoom, 1.0f); + Matrix matTranslation = MatrixTranslate(camera.offset.x, camera.offset.y, 0.0f); + Matrix matTransform = MatrixMultiply(MatrixMultiply(matOrigin, MatrixMultiply(matScale, matRotation)), matTranslation); + return matTransform; +} + // Set target FPS (maximum) void SetTargetFPS(int fps) { diff --git a/src/raylib.h b/src/raylib.h index 2e664fba8..4dcf78ac8 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -911,6 +911,9 @@ RLAPI void EndScissorMode(void); // End scissor RLAPI Ray GetMouseRay(Vector2 mousePosition, Camera camera); // Returns a ray trace from mouse position RLAPI Vector2 GetWorldToScreen(Vector3 position, Camera camera); // Returns the screen space position for a 3d world space position RLAPI Matrix GetCameraMatrix(Camera camera); // Returns camera transform matrix (view matrix) +RLAPI Vector2 GetScreenToWorld2D(Vector2 position, Camera2D camera); +RLAPI Vector2 GetWorldToScreen2D(Vector2 position, Camera2D camera); +RLAPI Matrix GetCamera2DMatrix(Camera2D camera); // Timing-related functions RLAPI void SetTargetFPS(int fps); // Set target FPS (maximum)