From 9bde204f1d7069e119312130577f36f5e8cecd2e Mon Sep 17 00:00:00 2001 From: PanicTitan <66186142+PanicTitan@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:33:43 -0300 Subject: [PATCH] [examples] Add textures_portal_window (#6082) Add a example showing a portal like effect --- examples/textures/textures_portal_window.c | 224 +++++++++++++++++++ examples/textures/textures_portal_window.png | Bin 0 -> 24992 bytes 2 files changed, 224 insertions(+) create mode 100644 examples/textures/textures_portal_window.c create mode 100644 examples/textures/textures_portal_window.png diff --git a/examples/textures/textures_portal_window.c b/examples/textures/textures_portal_window.c new file mode 100644 index 000000000..bf8cf6152 --- /dev/null +++ b/examples/textures/textures_portal_window.c @@ -0,0 +1,224 @@ +/******************************************************************************************* +* +* raylib [textures] example - portal window +* +* Example demonstrates rendering a second scene to a texture and projecting it onto a +* quad to create the illusion of a portal window looking into another place. The second +* scene is rendered with an off-axis ("oblique frustum") projection matched to the +* viewer's actual position relative to the window, so the illusion holds up correctly +* as the player moves and looks around, rather than only looking right from one spot +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by PanicTitan (@PanicTitan) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 PanicTitan (@PanicTitan) +* +********************************************************************************************/ + +#include "raylib.h" +#include "raymath.h" +#include "rlgl.h" +#include + +//------------------------------------------------------------------------------------ +// Module Functions Declaration +//------------------------------------------------------------------------------------ +static void BeginPortalMode3D(Vector3 eye, Vector3 bottomLeft, Vector3 bottomRight, Vector3 topLeft, float nearPlane, float farPlane); + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + InitWindow(screenWidth, screenHeight, "raylib [textures] example - portal window"); + + // Camera to navigate the "real world" (Dimension A) + Camera3D camera = { 0 }; + camera.position = (Vector3){ 0.0f, 2.5f, 7.0f }; + camera.target = (Vector3){ 0.0f, 1.8f, 0.0f }; + camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + camera.fovy = 45.0f; + camera.projection = CAMERA_PERSPECTIVE; + + // The archway opening, in Dimension A world space (used both to draw the frame and + // as the window rectangle the oblique projection is built from) + Vector3 archBottomLeft = { -1.5f, 0.0f, 0.0f }; + Vector3 archBottomRight = { 1.5f, 0.0f, 0.0f }; + Vector3 archTopLeft = { -1.5f, 4.0f, 0.0f }; + + // The archway sits at portalA and looks out onto portalB, far away in world space. + // Every frame, Dimension B gets rendered to a texture using the same relative eye + // and window position, shifted by the offset between the two portals + Vector3 portalA = { 0.0f, 0.0f, 0.0f }; + Vector3 portalB = { 0.0f, 0.0f, -60.0f }; + RenderTexture2D portalView = LoadRenderTexture(480, 640); + + DisableCursor(); // Lock cursor for first-person free camera controls + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(&camera, CAMERA_FREE); + float time = (float)GetTime(); + + // Eye and window corners, shifted into Dimension B so they match the player's + // actual position and viewing angle relative to the archway + Vector3 offset = Vector3Subtract(portalB, portalA); + Vector3 eyeInB = Vector3Add(camera.position, offset); + Vector3 blInB = Vector3Add(archBottomLeft, offset); + Vector3 brInB = Vector3Add(archBottomRight, offset); + Vector3 tlInB = Vector3Add(archTopLeft, offset); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + // Render Dimension B into an offscreen texture, using an oblique projection so + // its perspective lines up with the archway exactly as the real camera sees it + BeginTextureMode(portalView); + + ClearBackground((Color){ 10, 5, 20, 255 }); + + BeginPortalMode3D(eyeInB, blInB, brInB, tlInB, 0.05f, 100.0f); + + DrawGrid(30, 0.8f); + + // Floating pulsing core sphere + Vector3 corePos = Vector3Add(portalB, (Vector3){ 0.0f, 2.0f + sinf(time * 2.5f) * 0.4f, -4.0f }); + DrawSphere(corePos, 1.2f, PURPLE); + DrawSphereWires(corePos, 1.25f, 16, 16, MAGENTA); + + // Orbiting cubes + for (int i = 0; i < 4; i++) + { + float angle = time * 1.5f + i * (PI / 2.0f); + Vector3 pos = Vector3Add(portalB, (Vector3){ + sinf(angle) * 2.5f, + 2.0f + cosf(time * 3.0f + i) * 0.5f, + -4.0f + cosf(angle) * 2.5f }); + + DrawCube(pos, 0.5f, 0.5f, 0.5f, LIME); + DrawCubeWires(pos, 0.52f, 0.52f, 0.52f, DARKGREEN); + } + + EndMode3D(); + + EndTextureMode(); + + BeginDrawing(); + + ClearBackground((Color){ 15, 18, 26, 255 }); + + BeginMode3D(camera); + + DrawGrid(20, 1.0f); + + // Side pillars + DrawCube((Vector3){ -3.5f, 2.0f, 0.0f }, 0.8f, 4.0f, 0.8f, DARKGRAY); + DrawCubeWires((Vector3){ -3.5f, 2.0f, 0.0f }, 0.8f, 4.0f, 0.8f, ORANGE); + DrawCube((Vector3){ 3.5f, 2.0f, 0.0f }, 0.8f, 4.0f, 0.8f, DARKGRAY); + DrawCubeWires((Vector3){ 3.5f, 2.0f, 0.0f }, 0.8f, 4.0f, 0.8f, ORANGE); + + // Golden archway frame and solid base + DrawCubeWires((Vector3){ 0.0f, 2.0f, 0.0f }, 3.2f, 4.2f, 0.2f, GOLD); + DrawCube((Vector3){ 0.0f, 0.05f, 0.0f }, 3.4f, 0.1f, 0.6f, MAROON); + + // Solid backing wall, only ever seen if looking at the archway from behind + DrawCube((Vector3){ 0.0f, 2.0f, -0.05f }, 3.1f, 4.1f, 0.05f, DARKBLUE); + + // The portal opening itself: a plain quad textured with the Dimension B + // render, filling the archway exactly, so nothing "leaks" outside its shape + rlSetTexture(portalView.texture.id); + rlBegin(RL_QUADS); + rlColor4ub(255, 255, 255, 255); + rlNormal3f(0.0f, 0.0f, 1.0f); + rlTexCoord2f(0.0f, 0.0f); rlVertex3f(archBottomLeft.x, archBottomLeft.y, archBottomLeft.z); + rlTexCoord2f(1.0f, 0.0f); rlVertex3f(archBottomRight.x, archBottomRight.y, archBottomRight.z); + rlTexCoord2f(1.0f, 1.0f); rlVertex3f(archBottomRight.x, 4.0f, archBottomRight.z); + rlTexCoord2f(0.0f, 1.0f); rlVertex3f(archTopLeft.x, archTopLeft.y, archTopLeft.z); + rlEnd(); + rlSetTexture(0); + + EndMode3D(); + + // HUD overlay + DrawRectangle(15, 15, 400, 85, Fade(BLACK, 0.75f)); + DrawRectangleLines(15, 15, 400, 85, GOLD); + DrawText("PORTAL WINDOW", 28, 25, 20, GOLD); + DrawText("Look through the golden arch into Dimension B", 28, 52, 14, RAYWHITE); + DrawText("Controls: Mouse to look | WASD to move", 28, 72, 12, GRAY); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + UnloadRenderTexture(portalView); + + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} + +//---------------------------------------------------------------------------------- +// Module Functions Definition +//---------------------------------------------------------------------------------- + +// Starts a 3D mode using an off-axis ("oblique frustum") projection, built directly from +// an eye point and 3 corners of a rectangular window, instead of a fovy centered straight +// ahead of the camera. This is the standard technique for rendering a scene as seen through +// a window that isn't necessarily faced head-on (also used for multi-monitor and VR +// rendering) - the key difference from a normal Camera3D is that the frustum is allowed +// to be asymmetric, so perspective lines through the window line up correctly from any +// eye position, instead of behaving like a flat image pasted onto the window +static void BeginPortalMode3D(Vector3 eye, Vector3 bottomLeft, Vector3 bottomRight, Vector3 topLeft, float nearPlane, float farPlane) +{ + Vector3 right = Vector3Normalize(Vector3Subtract(bottomRight, bottomLeft)); + Vector3 up = Vector3Normalize(Vector3Subtract(topLeft, bottomLeft)); + Vector3 normal = Vector3Normalize(Vector3CrossProduct(right, up)); + + // Vectors from the eye to 3 corners of the window, used to project the window onto + // the near plane and read off how far it extends left/right/bottom/top of the eye + Vector3 toBL = Vector3Subtract(bottomLeft, eye); + Vector3 toBR = Vector3Subtract(bottomRight, eye); + Vector3 toTL = Vector3Subtract(topLeft, eye); + + float dist = -Vector3DotProduct(toBL, normal); + if (dist < 0.01f) dist = 0.01f; // Keep the eye from crossing the window plane + + float scale = nearPlane/dist; + + rlDrawRenderBatchActive(); + + rlMatrixMode(RL_PROJECTION); + rlPushMatrix(); + rlSetMatrixProjection(MatrixFrustum( + Vector3DotProduct(right, toBL)*scale, Vector3DotProduct(right, toBR)*scale, + Vector3DotProduct(up, toBL)*scale, Vector3DotProduct(up, toTL)*scale, + nearPlane, farPlane)); + + // View orientation is fixed to the window's own plane (looking straight through it + // along its normal), NOT aimed at any target - that's what the asymmetric frustum + // above is for, and is what lets the eye move off to one side without distorting + rlMatrixMode(RL_MODELVIEW); + rlLoadIdentity(); + rlMultMatrixf(MatrixToFloat(MatrixLookAt(eye, Vector3Subtract(eye, normal), up))); + + rlEnableDepthTest(); +} diff --git a/examples/textures/textures_portal_window.png b/examples/textures/textures_portal_window.png new file mode 100644 index 0000000000000000000000000000000000000000..902e7bb72376dbb5791e2225c6a403f6bfe78990 GIT binary patch literal 24992 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