Portal (window into another world)

포털(다른 세계로 창)

Rendering one scene to a texture first, then pasting it live onto a door, mirror, or window surface in another scene.

Also known as: Render-to-texture portalMagic windowWebGLRenderTarget scene
···
js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);

const rt = new THREE.WebGLRenderTarget(512, 512);

const innerScene = new THREE.Scene();
innerScene.background = new THREE.Color(0x2a0e08);
innerScene.fog = new THREE.FogExp2(0x2a0e08, 0.05);
const innerCamera = new THREE.PerspectiveCamera(55, 1, 0.1, 30);
innerCamera.position.set(0, 1, 6);
innerCamera.lookAt(0, 0, -1);
const innerLight = new THREE.DirectionalLight(0xffb37a, 1.8);
innerLight.position.set(2, 4, 3);
innerScene.add(innerLight);
innerScene.add(new THREE.AmbientLight(0x552211, 1.3));
const innerGroup = new THREE.Group();
for (let i = 0; i < 9; i++) {
  const m = new THREE.Mesh(new THREE.ConeGeometry(0.5, 1.2, 4), new THREE.MeshStandardMaterial({ color: 0xff8a3d, roughness: 0.5 }));
  const a = (i / 9) * Math.PI * 2;
  m.position.set(Math.cos(a) * 2.4, -0.2, Math.sin(a) * 2.4 - 1);
  innerGroup.add(m);
}
innerScene.add(innerGroup);

const mainScene = new THREE.Scene();
mainScene.background = new THREE.Color(0x05060c);
const mainCamera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
mainCamera.position.set(0, 0.3, 5.4);

const portalPlane = new THREE.Mesh(new THREE.CircleGeometry(1.35, 48), new THREE.MeshBasicMaterial({ map: rt.texture }));
mainScene.add(portalPlane);
const frame = new THREE.Mesh(new THREE.TorusGeometry(1.4, 0.09, 16, 100), new THREE.MeshBasicMaterial({ color: 0x6fe8ff }));
mainScene.add(frame);
const deco = new THREE.Group();
for (let i = 0; i < 5; i++) {
  const s = new THREE.Mesh(new THREE.IcosahedronGeometry(0.16, 0), new THREE.MeshBasicMaterial({ color: 0x4d5cff, wireframe: true }));
  s.position.set((Math.random() - 0.5) * 4.6, (Math.random() - 0.5) * 2.4, -0.6 - Math.random());
  deco.add(s);
}
mainScene.add(deco);

function resize() {
  renderer.setSize(innerWidth, innerHeight);
  mainCamera.aspect = innerWidth / innerHeight;
  mainCamera.updateProjectionMatrix();
}
addEventListener('resize', resize); resize();

renderer.setAnimationLoop((t) => {
  const time = t * 0.001;
  innerGroup.rotation.y = time * 0.3;
  innerCamera.position.x = Math.sin(time * 0.2) * 1.2;
  innerCamera.lookAt(0, 0, -1);
  frame.rotation.z = time * 0.1;
  deco.rotation.y = time * 0.15;
  mainCamera.position.x = Math.sin(time * 0.25) * 0.6;
  mainCamera.lookAt(0, 0, 0);
  renderer.setRenderTarget(rt);
  renderer.render(innerScene, innerCamera);
  renderer.setRenderTarget(null);
  renderer.render(mainScene, mainCamera);
});

three.js can point the renderer at an offscreen buffer — THREE.WebGLRenderTarget — instead of the canvas. This demo renders twice per frame: first the "inner world" (a small scene with its own background and lighting) into the render target (renderer.setRenderTarget(rt)), then it points the renderer back at the screen (null) to draw the "outer world," where a circular plane inside it wears that render target’s texture as its map.

Order matters: the inner scene must finish baking into the texture before the outer scene is drawn, so it has something to read. The two scenes are fully independent in camera, lighting, and background color, so it’s natural for whatever is beyond the portal to look like a different space with different physics.

The render target’s resolution (512×512 here) can stay fixed regardless of screen size, which makes performance easy to tune. In production the same trick builds security-camera feeds, mirrors, and screen-within-a-screen TVs — just remember each frame now draws an entire extra scene, so draw calls roughly double.

When to use

Portal or dimensional-doorway effects, security-camera and TV screens, minimaps — anywhere a different viewpoint needs to appear as a texture.