portal window
There is no browser build of this example yet.
Is it the same as C?
- Desktop:verified The Zig version renders a final frame that is byte-identical to the C example.
- Browser:not checked yet The browser comparison does not cover this example yet.
//! raylib [textures] example - portal window
//!
//! Zig-API translation of examples/textures/textures_portal_window.c (raylib 6.2).
//! Example contributed by PanicTitan (@PanicTitan) and reviewed by Ramon Santamaria (@raysan5)
//! Example licensed under an unmodified zlib/libpng license.
//! Copyright (c) 2025 PanicTitan (@PanicTitan)
const rl = @import("raylib");
const rm = rl.math;
pub fn main() !void {
// Initialization
const screen_width = 800;
const screen_height = 450;
rl.initWindow(screen_width, screen_height, "raylib [textures] example - portal window");
defer rl.closeWindow(); // Close window and OpenGL context
// Camera to navigate the "real world" (Dimension A)
var camera: rl.Camera3D = .{
.position = .{ .x = 0.0, .y = 2.5, .z = 7.0 },
.target = .{ .x = 0.0, .y = 1.8, .z = 0.0 },
.up = .{ .x = 0.0, .y = 1.0, .z = 0.0 },
.fovy = 45.0,
.projection = .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)
const arch_bottom_left: rl.Vector3 = .{ .x = -1.5, .y = 0.0, .z = 0.0 };
const arch_bottom_right: rl.Vector3 = .{ .x = 1.5, .y = 0.0, .z = 0.0 };
const arch_top_left: rl.Vector3 = .{ .x = -1.5, .y = 4.0, .z = 0.0 };
// 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
const portal_a: rl.Vector3 = .{ .x = 0.0, .y = 0.0, .z = 0.0 };
const portal_b: rl.Vector3 = .{ .x = 0.0, .y = 0.0, .z = -60.0 };
const portal_view = try rl.loadRenderTexture(480, 640);
defer rl.unloadRenderTexture(portal_view);
rl.disableCursor(); // Lock cursor for first-person free camera controls
rl.setTargetFPS(60);
// Main game loop
while (!rl.windowShouldClose()) { // Detect window close button or ESC key
// Update
rl.updateCamera(&camera, .free);
const time: f32 = @floatCast(rl.getTime());
// Eye and window corners, shifted into Dimension B so they match the player's
// actual position and viewing angle relative to the archway
const offset = rm.vector3Subtract(portal_b, portal_a);
const eye_in_b = rm.vector3Add(camera.position, offset);
const bl_in_b = rm.vector3Add(arch_bottom_left, offset);
const br_in_b = rm.vector3Add(arch_bottom_right, offset);
const tl_in_b = rm.vector3Add(arch_top_left, 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
{
rl.beginTextureMode(portal_view);
defer rl.endTextureMode();
rl.clearBackground(.{ .r = 10, .g = 5, .b = 20, .a = 255 });
beginPortalMode3D(eye_in_b, bl_in_b, br_in_b, tl_in_b, 0.05, 100.0);
defer endPortalMode3D();
rl.drawGrid(30, 0.8);
// Floating pulsing core sphere
const core_pos = rm.vector3Add(portal_b, .{ .x = 0.0, .y = 2.0 + @sin(time * 2.5) * 0.4, .z = -4.0 });
rl.drawSphere(core_pos, 1.2, .purple);
rl.drawSphereWires(core_pos, 1.25, 16, 16, .magenta);
// Orbiting cubes
for (0..4) |i| {
const fi: f32 = @floatFromInt(i);
const angle = time * 1.5 + fi * (rl.pi / 2.0);
const pos = rm.vector3Add(portal_b, .{
.x = @sin(angle) * 2.5,
.y = 2.0 + @cos(time * 3.0 + fi) * 0.5,
.z = -4.0 + @cos(angle) * 2.5,
});
rl.drawCube(pos, 0.5, 0.5, 0.5, .lime);
rl.drawCubeWires(pos, 0.52, 0.52, 0.52, .dark_green);
}
}
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(.ray_white);
{
rl.beginMode3D(camera);
defer rl.endMode3D();
rl.drawGrid(20, 1.0);
// Side pillars
rl.drawCube(.{ .x = -3.5, .y = 2.0, .z = 0.0 }, 0.8, 4.0, 0.8, .dark_gray);
rl.drawCubeWires(.{ .x = -3.5, .y = 2.0, .z = 0.0 }, 0.8, 4.0, 0.8, .orange);
rl.drawCube(.{ .x = 3.5, .y = 2.0, .z = 0.0 }, 0.8, 4.0, 0.8, .dark_gray);
rl.drawCubeWires(.{ .x = 3.5, .y = 2.0, .z = 0.0 }, 0.8, 4.0, 0.8, .orange);
// Golden archway frame and solid base
rl.drawCubeWires(.{ .x = 0.0, .y = 2.0, .z = 0.0 }, 3.2, 4.2, 0.2, .gold);
rl.drawCube(.{ .x = 0.0, .y = 0.05, .z = 0.0 }, 3.4, 0.1, 0.6, .maroon);
// Solid backing wall, only ever seen if looking at the archway from behind
rl.drawCube(.{ .x = 0.0, .y = 2.0, .z = -0.05 }, 3.1, 4.1, 0.05, .maroon);
// The portal opening itself: a plain quad textured with the Dimension B
// render, filling the archway exactly, so nothing "leaks" outside its shape
rl.gl.setTexture(portal_view.texture.id);
defer rl.gl.setTexture(0);
rl.gl.begin(rl.gl.quads);
defer rl.gl.end();
rl.gl.color4ub(255, 255, 255, 255);
rl.gl.normal3f(0.0, 0.0, 1.0);
rl.gl.texCoord2f(0.0, 0.0);
rl.gl.vertex3f(arch_bottom_left.x, arch_bottom_left.y, arch_bottom_left.z);
rl.gl.texCoord2f(1.0, 0.0);
rl.gl.vertex3f(arch_bottom_right.x, arch_bottom_right.y, arch_bottom_right.z);
rl.gl.texCoord2f(1.0, 1.0);
rl.gl.vertex3f(arch_bottom_right.x, 4.0, arch_bottom_right.z);
rl.gl.texCoord2f(0.0, 1.0);
rl.gl.vertex3f(arch_top_left.x, arch_top_left.y, arch_top_left.z);
}
// HUD overlay
rl.drawRectangle(15, 15, 520, 100, rl.fade(.black, 0.75));
rl.drawRectangleLines(15, 15, 520, 100, .gold);
rl.drawText("PORTAL WINDOW", 28, 25, 20, .gold);
rl.drawText("Look through the golden arch into Dimension B", 28, 52, 20, .ray_white);
rl.drawText("Controls: Mouse to look | WASD to move", 28, 80, 20, .gray);
}
}
/// 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
fn beginPortalMode3D(eye: rl.Vector3, bottom_left: rl.Vector3, bottom_right: rl.Vector3, top_left: rl.Vector3, near_plane: f32, far_plane: f32) void {
const right = rm.vector3Normalize(rm.vector3Subtract(bottom_right, bottom_left));
const up = rm.vector3Normalize(rm.vector3Subtract(top_left, bottom_left));
const normal = rm.vector3Normalize(rm.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
const to_bl = rm.vector3Subtract(bottom_left, eye);
const to_br = rm.vector3Subtract(bottom_right, eye);
const to_tl = rm.vector3Subtract(top_left, eye);
// Keep the eye from crossing the window plane
const dist = @max(-rm.vector3DotProduct(to_bl, normal), 0.01);
const scale = near_plane / dist;
rl.gl.drawRenderBatchActive();
rl.gl.matrixMode(rl.gl.projection);
rl.gl.pushMatrix();
rl.gl.setMatrixProjection(rm.matrixFrustum(
rm.vector3DotProduct(right, to_bl) * scale,
rm.vector3DotProduct(right, to_br) * scale,
rm.vector3DotProduct(up, to_bl) * scale,
rm.vector3DotProduct(up, to_tl) * scale,
near_plane,
far_plane,
));
// 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
rl.gl.matrixMode(rl.gl.modelview);
rl.gl.loadIdentity();
rl.gl.multMatrixf(&rm.matrixToFloatV(rm.matrixLookAt(eye, rm.vector3Subtract(eye, normal), up)).v);
rl.gl.enableDepthTest();
}
/// End portal 3D mode and returns to default 2D orthographic mode
/// NOTE: Similar implementation to endMode3D()
fn endPortalMode3D() void {
rl.gl.drawRenderBatchActive(); // Update and draw internal render batch
rl.gl.matrixMode(rl.gl.projection); // Switch to projection matrix
rl.gl.popMatrix(); // Restore previous matrix (projection) from matrix stack
rl.gl.matrixMode(rl.gl.modelview); // Switch back to modelview matrix
rl.gl.loadIdentity(); // Reset current matrix (modelview)
rl.gl.disableDepthTest(); // Disable DEPTH_TEST for 2D
}/*******************************************************************************************
*
* 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: [★★★★] 4/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"
//------------------------------------------------------------------------------------
// Module Functions Declaration
//------------------------------------------------------------------------------------
// Begin portal mode 3D view
static void BeginPortalMode3D(Vector3 eye, Vector3 bottomLeft, Vector3 bottomRight, Vector3 topLeft, float nearPlane, float farPlane);
// End portal 3D mode and returns to default 2D orthographic mode
static void EndPortalMode3D(void);
//------------------------------------------------------------------------------------
// 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);
}
EndPortalMode3D();
EndTextureMode();
BeginDrawing();
ClearBackground(RAYWHITE);
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, MAROON);
// 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, 520, 100, Fade(BLACK, 0.75f));
DrawRectangleLines(15, 15, 520, 100, GOLD);
DrawText("PORTAL WINDOW", 28, 25, 20, GOLD);
DrawText("Look through the golden arch into Dimension B", 28, 52, 20, RAYWHITE);
DrawText("Controls: Mouse to look | WASD to move", 28, 80, 20, 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();
}
// End portal 3D mode and returns to default 2D orthographic mode
// NOTE: Similar implementation to EndMode3D()
static void EndPortalMode3D(void)
{
rlDrawRenderBatchActive(); // Update and draw internal render batch
rlMatrixMode(RL_PROJECTION); // Switch to projection matrix
rlPopMatrix(); // Restore previous matrix (projection) from matrix stack
rlMatrixMode(RL_MODELVIEW); // Switch back to modelview matrix
rlLoadIdentity(); // Reset current matrix (modelview)
rlDisableDepthTest(); // Disable DEPTH_TEST for 2D
}From raylib 6.2. The original on GitHub.