npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

three-fenestra

v0.3.0

Published

Three.js plugin: interior mapping shader with optional PBR front layer. Drop-in MeshStandardMaterial subclass usable in vanilla Three.js and React Three Fiber.

Readme

Three-Fenestra: an interior mapping shader for Three.js

Status: v0.3.0, pre-1.0.

A MeshStandardMaterial subclass that adds parallax-based fake-3D rooms inside flat window planes, plus an optional PBR front layer for curtains, blinds, mullions, and glass dirt. Works in vanilla Three.js and React Three Fiber. Scales from one window to a whole instanced facade in a single draw call.

Three Fenestra: faux window interiors with interior mapping

Live demo: three-fenestra.codedgar.com


Why Three-Fenestra

The interior mapping technique was proposed by Joost van Dongen in 2008 to fake the look of furnished rooms behind window planes without modelling or lighting them. The original is one shader, one texture, no front layer.

Modern building renders need more than that: curtains that catch sun, glass that gets dirty, mullions that cast shadow, windows that switch from "lit" to "dark" by time of day. Three-Fenestra keeps the cheap ray-march at the core and stacks the modern bits on top:

  • A back atlas of interior rooms (the original technique)
  • A PBR front overlay with optional normal / roughness / metalness atlases
  • A transmission term so curtains can still bleed warm light at night
  • Uniforms wired for a day/night controller (you supply the controller)
  • GPU-side per-window lights — lit/unlit, warm/cool, brightness jitter, all derived from a window seed with no per-window CPU work (emissiveVariation)
  • An instanced mode so thousands of windows render from one InstancedMesh and one material in a single draw call
  • A LOD blend that collapses to a flat impostor at distance, for crossfading against cheap far-LOD facades

Drop it into any existing scene that already uses Three's standard lighting and it composites correctly — one material per window mesh, or one material for a whole instanced facade.


Install

npm install three-fenestra three

three >= 0.150 is a peer dependency.

Starter atlases

The package ships two ready-to-use 4×4 atlases under three-fenestra/starter/ so you don't have to author your own to see something render on day one:

| Path | What it is | |---|---| | three-fenestra/starter/rooms.webp | Back atlas — 4×4 grid of interior rooms. | | three-fenestra/starter/overlay.webp | Front atlas — 4×4 grid of curtain / blind variants with alpha. |

Any modern bundler (Vite, webpack, Parcel, esbuild) imports them as URLs. Once you outgrow the starters, follow Creating your own atlases.


Quick start (vanilla Three.js)

import * as THREE from 'three';
import { InteriorMappingMaterial } from 'three-fenestra';
import roomsUrl   from 'three-fenestra/starter/rooms.webp';
import overlayUrl from 'three-fenestra/starter/overlay.webp';

const atlas = new THREE.TextureLoader().load(roomsUrl);
atlas.colorSpace = THREE.SRGBColorSpace;
atlas.wrapS = atlas.wrapT = THREE.ClampToEdgeWrapping;

const material = new InteriorMappingMaterial({
  backAtlas: atlas,
  backAtlasCols: 4,
  backAtlasRows: 4,
  depth: 1.0,
  backScale: 0.66,
  planeSize: new THREE.Vector2(width, height),
  windowId: new THREE.Vector3(x, y, z), // per-window seed for cell picking
  roughness: 0.15,
  metalness: 0.0,
});

const mesh = new THREE.Mesh(new THREE.PlaneGeometry(width, height), material);

The interior renders with no front textures. Add the PBR front layer at any time — the starter overlay.png is a 4×4 curtain atlas you can drop in:

const overlay = new THREE.TextureLoader().load(overlayUrl);
overlay.colorSpace = THREE.SRGBColorSpace;
material.setFrontAtlas(overlay, 4, 4);

// Optional companion PBR maps if you have your own:
material.setFrontNormalAtlas(curtainNormal, 1);   // samples .xy
material.setFrontRoughnessAtlas(curtainRough);    // samples .g
material.setFrontMetalnessAtlas(curtainMetal);    // samples .b

Where front alpha is 0, you see the interior through the "glass." Where it is 1, you see the front layer lit by scene lights via standard PBR, fresnel included.


React Three Fiber

R3F passes constructor arguments via args as a single-element array (the material takes one options object):

import { extend, type ThreeElement } from '@react-three/fiber';
import { InteriorMappingMaterial } from 'three-fenestra';

extend({ InteriorMappingMaterial });

declare module '@react-three/fiber' {
  interface ThreeElements {
    interiorMappingMaterial: ThreeElement<typeof InteriorMappingMaterial>;
  }
}

<mesh>
  <planeGeometry args={[width, height]} />
  <interiorMappingMaterial
    args={[{
      backAtlas: atlas,
      planeSize: new THREE.Vector2(width, height),
      windowId: new THREE.Vector3(x, y, z),
    }]}
  />
</mesh>

Setting individual props on <interiorMappingMaterial /> after construction works for the runtime knobs (depth, backScale, interiorEmissive, frontTransmission, frontAlphaBoost) because those are wired to setters. Texture swaps should go through setFrontAtlas(...) / setBackAtlas(...) via a ref.


API

Constructor parameters

All MeshStandardMaterialParameters are accepted, plus:

| Parameter | Type | Default | Description | |---|---|---|---| | backAtlas | Texture | required | The interior (rooms) atlas. | | backAtlasCols, backAtlasRows | number | 4, 4 | Grid dimensions of the back atlas. | | depth | number | 1.0 | Apparent room depth, in plane-local units. | | backScale | number | 0.66 | Back-wall fill factor (0.05–0.999). | | planeSize | Vector2 | required | Must match the geometry's width × height. | | windowId | Vector3 | required¹ | Per-window seed (typically the window center) for atlas cell picking. ¹Optional in instanced mode, where instanceWindowId replaces it. | | interiorEmissive | Color | (1, 1, 1) | Multiplier on the interior contribution before adding to the lit output. Use to tint warm and scale up for "lights on" night mode (e.g. new Color(2, 1.5, 1)). Overridden by emissiveVariation when that is set. | | emissiveVariation | EmissiveVariation? | — | GPU-side per-window lights derived from windowId. See Per-window lights. | | lod | number | 1 | Level-of-detail blend in [0, 1]. 1 = full interior mapping + glass cues; 0 = flat impostor (no parallax/fresnel/refraction). See LOD. | | instanced | boolean | false | Enable instanced mode: per-window data comes from instanced attributes instead of uniforms. | | frontAtlas | Texture? | — | Front overlay atlas (RGBA: color + alpha). | | frontAtlasCols, frontAtlasRows | number | 1, 1 | Front atlas grid. | | frontNormalAtlas | Texture? | — | Tangent-space normal map atlas. | | frontNormalScale | number | 1.0 | Multiplier on .xy of the normal sample. | | frontRoughnessAtlas | Texture? | — | Roughness atlas (samples .g). | | frontMetalnessAtlas | Texture? | — | Metalness atlas (samples .b). | | frontTransmission | number | 0.25 | Fraction of interior light that bleeds through the opaque front layer, tinted by the front color. 0 = front fully blocks interior, 1 = no blocking. | | frontAlphaBoost | number | 1.0 | Raises effective opacity of the front layer (pow(alpha, 1/boost)). > 1 makes semi-transparent pixels read as more opaque without re-authoring the texture. |

Glass surface (optional)

These give the glass area (where the front layer is transparent) the look of a real pane: dirt, refraction, fresnel sheen. All default to zero / off; turn on the ones you want.

| Parameter | Type | Default | Description | |---|---|---|---| | glassThickness | number | 0 | Apparent glass thickness in plane-local units. Parallax-shifts the front-overlay sample so it appears to sit on the inside face of the pane rather than glued to the outside surface. 0 disables. | | refractionStrength | number | 0 | Magnitude (in cell-UV units) of the interior ray-march perturbation driven by glassDirtMap. Sells the "looking through real glass" effect. Keep tiny — typical range 0.0030.015. 0 disables. | | glassDirtMap | Texture? | — | Grayscale noise texture used as the dirt/specular modulator over the glass area, and as the source of the refraction perturbation. Centered around 0.5; values > 0.5 roughen the glass, < 0.5 polish it. | | glassDirtStrength | number | 0.35 | How strongly the dirt map modulates roughness on the glass area. | | glassFresnelStrength | number | 0 | Schlick fresnel sheen added to the glass at grazing angles. Primary "this is a pane of glass" cue. Demo uses ~0.5. | | glassFresnelColor | Color | (0.85, 0.92, 1.0) | Tint of the fresnel sheen. Cool white reads as sky reflection. | | glassSmudgeStrength | number | 0 | Additive brightness of dirt visible as smudges on the glass surface. Different from glassDirtStrength (roughness modulation). |

Runtime setters

// Core knobs
material.depth              = 0.8;
material.backScale          = 0.6;
material.interiorEmissive   = new THREE.Color(2.0, 1.5, 1.0);  // copies into uniform
material.frontTransmission  = 0.10;
material.frontAlphaBoost    = 1.0;

// Glass-surface knobs
material.glassThickness       = 0.04;
material.refractionStrength   = 0.005;
material.glassDirtStrength    = 0.35;
material.glassFresnelStrength = 0.5;
material.glassFresnelColor    = new THREE.Color(0.85, 0.92, 1.0);  // copies into uniform
material.glassSmudgeStrength  = 0.1;

// LOD blend (1 = full detail, 0 = flat impostor)
material.lod = 0.5;

// Per-window lights — pass params to enable/update, or null to disable
material.setEmissiveVariation({ litRatio: 0.6, brightMin: 0.5, brightRange: 0.8 });
material.setEmissiveVariation(null);  // back to flat interiorEmissive

// Texture swaps (pass null to disable)
material.setBackAtlas(newAtlas);
material.setFrontAtlas(tex, cols, rows);
material.setFrontNormalAtlas(tex, scale);
material.setFrontRoughnessAtlas(tex);
material.setFrontMetalnessAtlas(tex);
material.setGlassDirtMap(tex);

Day / night

The library does not ship a day/night controller. It gives you the uniforms to build one. Typical recipe:

const day   = { emissive: new THREE.Color(1, 1, 1),         transmission: 0.15 };
const night = { emissive: new THREE.Color(1.7, 1.35, 0.95), transmission: 0.10 };

function setMode(p: typeof day) {
  for (const m of materials) {
    m.interiorEmissive  = p.emissive;
    m.frontTransmission = p.transmission;
  }
}

For full "lights on at night," pair this with:

  • Reduced scene ambient and sun, but not to zero. Building exteriors at night still receive skyglow, streetlights, and reflections. Curtain colours need ambient to read as fabric, not as backlit cutouts.
  • A cool-tinted ambient with a warm interior emissive for the classic night-city contrast.
  • UnrealBloomPass on the composer (low strength, around 0.4) to spill the lit-window contribution onto neighbouring pixels.

The bundled examples/asia-building demo wires all three; check main.ts for a working reference.


Per-window lights (emissiveVariation)

Instead of one shared interiorEmissive, let the GPU give every window its own state — lit or unlit, warm or cool, dim or bright — derived entirely from windowId. No per-window CPU work, no per-window uniforms: the same seed always produces the same window, so a far-LOD impostor can reproduce it exactly.

const material = new InteriorMappingMaterial({
  backAtlas: atlas,
  planeSize,
  windowId,
  emissiveVariation: {
    litRatio: 0.5,   // chance a window is lit
    warm: new THREE.Color(1.7, 1.35, 0.95),  // incandescent tint
    cool: new THREE.Color(0.9, 1.15, 1.5),   // fluorescent / TV tint
    coolChance: 0.22,                         // chance a lit window is cool
    brightMin: 0.3, brightRange: 0.35,        // lit brightness = min + range·rand
    dim: new THREE.Color(0.07, 0.08, 0.1),   // unlit (faintly visible dark room)
  },
});

// Update or disable at runtime (e.g. ramp litRatio toward night):
material.setEmissiveVariation({ litRatio: 0.7 });
material.setEmissiveVariation(null);  // fall back to flat interiorEmissive

All fields are optional and fall back to the defaults shown above. When emissiveVariation is set it overrides interiorEmissive. The hash seeds are part of the public contract (lit 3.71, tone 9.27, brightness 5.43) so an external shader can reproduce identical windows from the same id.

To crossfade smoothly from a flat daytime look into night without a pop, keep it enabled the whole time and drive litRatio from 0 (day — every window returns dim, i.e. a uniform look) up toward your night value. The examples/asia-building demo does exactly this.


Level of detail (LOD)

lod blends between full interior mapping and a flat impostor:

  • lod = 1 — full parallax ray-march plus all glass cues (fresnel, refraction, smudges, glass thickness).
  • lod = 0 — the back atlas cell is sampled straight onto the plane with no parallax and the glass cues shut off. Pixel-equivalent to a cheap facade shader sampling the same cell.

Drive it by camera distance to crossfade a near, full-detail material against a far, cheap one:

const d = camera.position.distanceTo(windowWorldPos);
material.lod = THREE.MathUtils.clamp(1 - (d - near) / (far - near), 0, 1);

In instanced mode the per-instance instanceLod attribute replaces this, and instanceFade scales output alpha so you can crossfade whole LOD tiers (set transparent: true).


Instancing

For high window counts, instanced: true moves per-window data off uniforms and onto InstancedBufferAttributes, so one InstancedMesh + one material draws thousands of windows in a single draw call.

The geometry must carry three instanced attributes:

| Attribute | Type | Replaces | Purpose | |---|---|---|---| | instanceWindowId | vec3 | windowId | Per-window seed (cell pick + emissive variation). | | instanceLod | float | lod | Per-window LOD blend. | | instanceFade | float | — | Scales output alpha (set transparent: true for LOD crossfades). |

const geom = new THREE.PlaneGeometry(W, H);
const ids = new Float32Array(count * 3);
const lods = new Float32Array(count).fill(1);
const fades = new Float32Array(count).fill(1);

const mesh = new THREE.InstancedMesh(geom, material, count);
for (let i = 0; i < count; i++) {
  mesh.setMatrixAt(i, matrixForWindow(i));   // position/orient each window
  ids.set([seedX(i), seedY(i), 0], i * 3);
}
geom.setAttribute('instanceWindowId', new THREE.InstancedBufferAttribute(ids, 3));
geom.setAttribute('instanceLod',      new THREE.InstancedBufferAttribute(lods, 1));
geom.setAttribute('instanceFade',     new THREE.InstancedBufferAttribute(fades, 1));

const material = new InteriorMappingMaterial({
  backAtlas: atlas, planeSize: new THREE.Vector2(W, H),
  instanced: true,            // windowId is optional here
  emissiveVariation: { litRatio: 0.5 },
});

instanced: true must be paired with an actual THREE.InstancedMesh — the shader reads the instanceMatrix Three provides for instanced meshes. See examples/instanced/main.ts for a complete 640-window facade.


Embedding the shader (glslCore)

The interior-mapping math is also exported as a standalone, varying-free GLSL string for embedding into arbitrary materials (a building shell, a merged city mesh) via onBeforeCompile — not just the bundled plane material:

import { glslCore } from 'three-fenestra';

myMaterial.onBeforeCompile = (shader) => {
  shader.fragmentShader = shader.fragmentShader
    .replace('#include <common>', `#include <common>\n${glslCore}`);
  // now call imWindowEmissive(...), imRoomBoxUV(...), imAtlasUV(...), imHash(...)
};

The functions (imHash, imWindowEmissive, imAtlasUV, imRoomBoxUV) take all their inputs as parameters, so you supply your own uniforms/varyings. This is what the bundled material itself delegates to — one implementation, every consumer.


Extending

The library ships mechanism, not policy. InteriorMappingMaterial works out of the box for the common case (windows on a mesh, day/night, glass). But anything scene-specific is left to you, because the right answer depends on your scene, not the material. What you get are composable building blocks:

| You want… | The block | What you build on top | |---|---|---| | Interior mapping in a non-plane material | glslCore | Inject into any shader via onBeforeCompile. | | Distance-based LOD | lod / instanceLod / instanceFade | Your own distance → lod mapping and tier crossfade. | | A cheap far-LOD impostor that matches | the seed contract (below) | A flat shader that reproduces the same windows from the same id. | | City-scale facades | instanced mode | Your own instance attribute streams / culling. | | GPU-driven day/night lights | emissiveVariation | A controller that ramps litRatio over time. |

There is deliberately no built-in LOD manager or impostor shader. LOD strategy (thresholds, tier count, crossfade vs. pop, how it hooks your camera and culling) is app-specific; baking it in would only fight real integrations.

Stability: the hash-seed contract

Per-window appearance is derived entirely from the window seed by imHash, using these seed constants:

| Aspect | Seed | |---|---| | Lit / unlit | 3.71 | | Warm / cool tone | 9.27 | | Brightness jitter | 5.43 |

These are public API. A far-LOD impostor shader can hash the same windowId with the same seeds and reproduce the exact same window, which is what makes seamless LOD crossfades possible. Because downstream shaders may depend on them, these values will not change without a major version bump. Treat them as a stable contract, not an implementation detail.


Creating your own atlases

Two textures drive the look: the back atlas (interior rooms) and the optional front atlas (curtains, blinds, glass overlays). Both are grids of square cells; the shader picks a cell per window using a deterministic hash of windowId, so the same window always gets the same room across re-renders.

Back atlas (interior rooms)

A grid of square room photos. Each cell is one "room" the ray-march will land you inside.

| Spec | Recommendation | |---|---| | Grid | 4×4 (16 variants) is the sweet spot for masking repetition across hundreds of windows. 2×2 is fine for small scenes. | | Cell aspect | Square (1:1). The ray-march assumes a unit cube per cell. | | Image size | Power of two (1024×1024, 2048×2048). Lets Three generate mipmaps. | | Color space | sRGB. Set texture.colorSpace = SRGBColorSpace so sampling converts to linear for PBR. | | Edge bleed | The shader insets each cell by 0.001 to prevent bleed; keep ~2 px gutter inside each cell as insurance. | | Wrap | ClampToEdgeWrapping on both axes. | | Filter | LinearMipmapLinearFilter (min) + LinearFilter (mag), anisotropy: 8+. | | Content | Frame each cell as if looking through a window from outside. Centre the composition; the back wall should fill ~60–70% of the cell (matches default backScale). Already-lit photography works best; the shader treats interior pixels as pre-lit. |

Front atlas (curtains, blinds, overlays)

A grid of window dressings. Each cell sits on top of one window using the same cell-picking logic.

| Spec | Recommendation | |---|---| | Grid | Match the variety you want. 4×4 = 16 variants. | | Cell aspect | Square. Real windows are not square; the shader stretches the cell to the window's actual aspect, so pick curtain compositions that survive a moderate stretch. | | Format | PNG with alpha (RGBA). | | Color space | sRGB. | | Trim to edge | Each curtain should fill its cell edge-to-edge with no transparent gutter. If your source has padding, trim it:magick in.png -alpha set -fuzz 10% -bordercolor none -border 1 -trim +repage -resize 256x256^ -gravity center -extent 256x256 out.png | | Alpha encoding | The single biggest authoring decision. Opaque (alpha = 1) is curtain fabric. Transparent (alpha = 0) is the glass area you want the interior to show through. Anywhere between is "semi-sheer" and the shader reads it as fractional transmission. |

Sheer and lace curtains

Do not author the fabric itself at low alpha unless you genuinely want light to pour through it. Anti-aliased edges are fine; intentional partial transparency on every pixel of the curtain is what causes the "windows evaporate at night" problem.

If you already have a texture with semi-transparent fabric and want it to behave more solidly at night, raise frontAlphaBoost (try 2.02.5). It is a render-time knob; no re-export needed.

Front PBR maps (optional)

If you want the curtain fabric to receive proper PBR lighting (fresnel, scene light response):

  • Normal atlas: tangent-space, same grid as the albedo atlas. RGB channels = XYZ, encoded [0..1] mapping to [-1..1].
  • Roughness atlas: single-channel; the shader samples .g. White = rough, black = mirror.
  • Metalness atlas: single-channel; the shader samples .b. Curtains and glass are non-metallic, so almost always 0.

All three must share the front albedo atlas's grid dimensions.

Window mesh setup

Each window is a PlaneGeometry sized to the real window dimensions. Three things every material needs:

  1. planeSize: the geometry's (width, height) as a Vector2. The shader uses it to normalise object-space position into local UV.
  2. windowId: a Vector3 unique per window. The window's centre in world space is a natural choice.
  3. The plane's local +Z must be the outward-facing normal. If your geometry comes from a model with arbitrary orientation, build a basis from (right, up, normal) and apply it via mesh.quaternion.setFromRotationMatrix(makeBasis(right, up, normal)).

See examples/asia-building/main.ts for a complete example pulling per-window data from a JSON descriptor.

Helper scripts

The examples/asia-building/tools/ folder has small Python scripts used to build the demo's atlases:

  • detect_windows.py, extract_windows.py: pull window crops from a facade photo
  • analyze_atlas.py: sanity-check cell layout and channel content
  • glass_dirt.svg: source for the glass-dirt overlay used in the demo

They are unsupported, not packaged, and exist as references. Adapt or ignore.


Limitations and roadmap

  • No envmap / cubemap reflections on the glass area. Would require fresnel-modulated env sampling.
  • Pre-lit interior. The atlas is treated as already-shaded photography; scene lights do not relight the interior. This is by design; relighting fake rooms would defeat the cost saving the technique exists for.

Development

npm install
npm run dev          # serves examples/asia-building on :5173
npm run dev:instanced # serves examples/instanced (640-window facade)
npm run dev:minimal  # serves examples/minimal (single window)
npm run build        # produces dist/ (the publishable package)
npm run build:demo   # produces dist-demo/ (static export of the demo)
npm run typecheck

Examples

  • examples/asia-building/ — the full demo: 160 windows on a real building, cinematic camera, day/night palette with per-window lights, glass dirt, PBR curtains, LOD slider. What npm run dev serves and what powers the live demo.
  • examples/instanced/ — a 640-window facade from one InstancedMesh and one material in a single draw call, with per-window emissive variation. The reference for instanced mode.
  • examples/minimal/ — single window plane, ~60 lines. The shortest runnable example for understanding the API surface.

Credits

  • Joost van Dongen, Interior Mapping: A new technique for rendering realistic buildings (2008).
  • The Three.js team for MeshStandardMaterial and the onBeforeCompile hook this material extends.

License

MIT.