# GlyphScene

`<GlyphScene>` is the visual host. It owns the `<pre>` output element, the sibling
hit layer, and the measured cell metrics the projection uses.

`<GlyphScene>` must be a child of a camera component (`<GlyphCamera>`,
`<GlyphPerspectiveCamera>`, or `<GlyphOrthographicCamera>`).



```ts
import { createGlyphPerspectiveCamera, createGlyphScene, createGlyphOrbitControls } from "glyphcss";
import { resolveGeometry } from "@glyphcss/core";

const host = document.getElementById("scene");
const camera = createGlyphPerspectiveCamera({ rotX: 65, rotY: 0, zoom: 0.3, distance: 0, perspective: 32000, stretch: 1 });
const scene = createGlyphScene(host, { camera, autoSize: true, mode: "wireframe", charMode: "braille" });
scene.output.style.fontSize = "10px";
scene.output.style.lineHeight = "1";
scene.fit();
const polygons = resolveGeometry("cuboctahedron", { size: 100 });
scene.add(polygons);
createGlyphOrbitControls(scene, { drag: true, wheel: true });
```



## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `mode` | `"wireframe" \| "solid" \| "voxel" \| "ink"` | `"solid"` | Render mode |
| `cols` | `number` | `80` | Grid width in character columns |
| `rows` | `number` | `24` | Grid height in character rows |
| `cellAspect` | `number` | `2.0` | Character cell height ÷ width |
| `glyphPalette` | `string` | `"default"` | Named glyph palette |
| `autoSize` | `boolean` | `false` | Measure the host element and derive `cols`/`rows` from it instead of using the fixed grid |
| `charMode` | `"ascii" \| "braille" \| "halfblock" \| "quadrant"` | `"ascii"` | Sub-cell character encoding. `"braille"` is wireframe-only; `"halfblock"` and `"quadrant"` are solid-only. No-op outside its mode. See [character encoding](/guides/render-modes/#character-encoding-charmode) |
| `wireframeJunctions` | `boolean` | `false` | Resolve wireframe corners and crossings to box-drawing glyphs (`┌┐└┘├┤┬┴┼─│`). ASCII wireframe only. See [junctions](/guides/render-modes/#box-drawing-junctions-wireframejunctions) |
| `hiddenLines` | `"show" \| "hide"` | `"show"` | Hidden-line removal for `wireframe` and `ink`: `"hide"` drops strokes a nearer surface covers. No-op in `solid`. See [hidden-line removal](/guides/render-modes/#hidden-line-removal-hiddenlines) |
| `solidWeightRamp` | `{ glyph: string; weight: number }[]` | `undefined` | Solid-only shading ramp that varies `font-weight` as well as glyph, ordered darkest → densest. Replaces the palette's solid ramp when set. See [font-weight ramp](/guides/render-modes/#font-weight-density-ramp-solidweightramp) |
| `colorTolerance` | `number` | `0` | Merge adjacent cells into one `<span>` while their colors stay within this redmean distance (range `0`–`765`, not `0`–`255`) — fewer spans, faster paint, at the cost of color fidelity. `0` is off and byte-identical. `NaN`/negative values degrade to `0`; `+Infinity` merges every same-glyph run in a row. No-op under `glyphOutput: "semantic"` — semantic colors are exact class identifiers, not shaded appearance. See [spans, not just cells](/guides/performance/#spans-not-just-cells-colortolerance) |
| `colorEncoding` | `"spans" \| "atlas"` | `"spans"` | `"atlas"` encodes each `(glyph, color)` pair as a Private Use Area code point against a checked-in COLR/CPAL color font, so the render is one text node with zero `<span>`s. `"spans"` is byte-identical to before the option existed. Falls back to spans, whole-scene, whenever the atlas cannot carry the frame. See [color encoding](/guides/color-encoding) |
| `atlasPalette` | `readonly string[]` | `undefined` | Pin the ordered `#rrggbb` palette that `colorEncoding: "atlas"` slots encode against — a brand ramp or a reproducible bake. Omitted, the scene derives and pools one itself, so this does **not** gate the atlas. Bounded by the atlas's slot count. See [the palette](/guides/color-encoding/#the-palette-atlaspalette) |
| `fontAtlas` | `GlyphFontAtlas` | `GLYPH_FONT_ATLAS` | Which color-font atlas to encode against — the universal one (212 glyphs / 30 palette slots) or `GLYPH_FONT_ATLAS_ASCII` (94 / 68), trading glyph coverage for color resolution. **Fixed at scene creation**; a later change is not forwarded. See [choosing an atlas](/guides/color-encoding/#choosing-an-atlas-fontatlas) |
| `smoothShading` | `boolean` | `false` | Gouraud shading from averaged vertex normals. Off by default — the faceted look is part of glyph's identity |
| `creaseAngle` | `number` | `60` | Max angle (degrees) between adjacent faces still smoothed together when `smoothShading` is on |
| `interactiveDownscale` | `number` | `1` | Render at `1/n` resolution while a control is dragging, full detail on release. Same on-screen size — this keeps high-density scenes inside the frame budget mid-gesture |
| `trackOpaqueCoverage` | `boolean` | `false` | Force a base-layer depth raster each render so `scene.getOpaqueCoverage()` can publish it. A scene only builds an occlusion id-map when it has an opaque detail mesh or a foreign mask, so a scene of plain base meshes needs this to feed another scene's `setForeignOcclusion`. Changes no output; costs one raster |
| `glyphOutput` | `"visible" \| "semantic"` | `"visible"` | `"semantic"` renders authored class labels instead of shaded glyphs and enables `scene.getGlyphSemanticCellFrame()`. Requires the `sceneManifest` + `dictionary` JS properties |
| `useColors` | `boolean` | `true` | Emit color spans in the output |
| `directionalLight` | `GlyphDirectionalLight` | — | Directional light for solid mode |
| `ambientLight` | `GlyphAmbientLight` | — | Ambient fill for solid mode |
| `shadow` | `GlyphShadowOptions` | `undefined` | Shadow-map config. `undefined` = off. Set alongside `castShadow`/`receiveShadow` on meshes |
| `transformCells` | `TransformCells` | `undefined` | Transform the completed cell grid before its single `<pre>` write |
| `className` | `string` | — | CSS class on the outer host |
| `style` | `CSSProperties` | — | Inline styles on the outer host |
| `children` | `ReactNode` | — | `<GlyphMesh>`, controls, hotspots |

### Vanilla-only scene options

`createGlyphScene` accepts a few options the React and Vue components do not
expose as props. `compileScene` accepts the first two (`doubleSided` and
`supersample`) as well:

| Option | Default | What it does |
|---|---|---|
| `doubleSided` | `false` | Shade back faces instead of culling them — needed for open meshes and flat planes viewed from behind |
| `supersample` | `1` | Rasterize at `n×` the cell grid and box-filter down, for coverage antialiasing. `charMode: "halfblock"`/`"quadrant"` force an even supersample of at least 2 internally |
| `depthEpsilon` | `0` | Depth-test tolerance for coplanar surfaces |
| `temporalBlend` | `0` | Reprojection TAA: blends the ramp index and RGB against the previous frame. `solidWeightRamp` and `charMode: "halfblock"`/`"quadrant"` are no-ops while it is active |

## Cell transforms and surface UVs

`transformCells` runs after rasterization, shading, and depth testing, immediately
before glyphcss stringifies the grid. The hook may mutate `grid.char` and
`grid.color`; glyphcss still performs one write to the scene's `<pre>` for the
completed frame.

In solid mode, polygons with authored `uvs` also expose `grid.surfaceUv`. This is
an optional interleaved `Float32Array` containing the perspective-correct UV from
the depth-winning surface for each cell:

```ts
import { createGlyphScene } from "glyphcss";
import type { TransformCells } from "glyphcss";

const word = Array.from("HOLA");

const mapWordToSurface: TransformCells = (grid) => {
  const uv = grid.surfaceUv;
  if (!uv) return;

  for (let i = 0; i < grid.char.length; i++) {
    const u = uv[i * 2];
    const v = uv[i * 2 + 1];
    if (!Number.isFinite(u) || !Number.isFinite(v)) continue;

    grid.char[i] = word[((Math.floor(u * 12) % word.length) + word.length) % word.length];
  }
};

const scene = createGlyphScene(host, {
  camera,
  mode: "solid",
  transformCells: mapWordToSurface,
});
```

`surfaceUv` uses `[u0, v0, u1, v1, ...]`. Empty cells and cells whose winning
polygon has no UVs contain `NaN`/non-finite coordinates, so effects must check
both components. Values remain in the polygon's authored coordinate space;
they are not clamped to `0..1`, and may tile beyond that range. Because the
mapping follows the polygon UVs rather than screen rows and columns, patterns
rotate and foreshorten with the surface. Treat the grid buffers as
callback-scoped; use `rasterizeToCells` when they must outlive the synchronous
`transformCells` call.

For reusable or animated appearance, prefer a mounted
[`GlyphEffectLayer`](/guides/effects). Generic layers retain the geometry raster
and avoid re-projecting the mesh on parameter-only ticks; `transformCells`
remains the final application-specific escape hatch and runs after those layers.

## Full examples

```tsx
import {
  GlyphPerspectiveCamera,
  GlyphScene,
  GlyphMesh,
  GlyphOrbitControls,
  GlyphHotspot,
} from "@glyphcss/react";
import { cubePolygons } from "@glyphcss/core";

const cube = cubePolygons({ center: [0, 0, 0], size: 1, color: "#4488ff" });

export function App() {
  return (
    <GlyphPerspectiveCamera rotX={25} zoom={50} distance={3}>
      <GlyphScene mode="solid" cols={100} rows={30}>
        <GlyphOrbitControls drag wheel />
        <GlyphMesh polygons={cube}>
          <GlyphHotspot
            id="top"
            at={[0, 0.5, 0]}
            size={[3, 2]}
            onClick={() => alert("top face")}
          />
        </GlyphMesh>
      </GlyphScene>
    </GlyphPerspectiveCamera>
  );
}
```

  

```vue
<template>
  <GlyphPerspectiveCamera :rot-x="25" :zoom="50" :distance="3">
    <GlyphScene mode="solid" :cols="100" :rows="30">
      <GlyphOrbitControls drag wheel />
      <GlyphMesh :polygons="cube">
        <GlyphHotspot
          id="top"
          :at="[0, 0.5, 0]"
          :size="[3, 2]"
          @click="() => alert('top face')"
        />
      </GlyphMesh>
    </GlyphScene>
  </GlyphPerspectiveCamera>
</template>

<script setup lang="ts">
import {
  GlyphPerspectiveCamera,
  GlyphScene,
  GlyphMesh,
  GlyphOrbitControls,
  GlyphHotspot,
} from "@glyphcss/vue";
import { cubePolygons } from "@glyphcss/core";

const cube = cubePolygons({ center: [0, 0, 0], size: 1, color: "#4488ff" });
</script>
```

  

```ts
import {
  createGlyphPerspectiveCamera,
  createGlyphScene,
  createGlyphOrbitControls,
} from "glyphcss";
import { cubePolygons } from "@glyphcss/core";

const host = document.querySelector<HTMLElement>("#scene")!;

const camera = createGlyphPerspectiveCamera({ rotX: 25, zoom: 50, distance: 3 });
const scene = createGlyphScene(host, {
  camera,
  mode: "solid",
  cols: 100,
  rows: 30,
});

scene.add(cubePolygons({ center: [0, 0, 0], size: 1, color: "#4488ff" }));

scene.addHotspot(
  { id: "top", at: [0, 0.5, 0], size: [3, 2] },
  () => alert("top face"),
);

const controls = createGlyphOrbitControls(scene, { drag: true, wheel: true });

// Later, when done:
// controls.destroy();
// scene.destroy();
```

  

```html
<script type="module" src="https://esm.sh/glyphcss/elements"></script>

<glyph-camera>
  <glyph-scene mode="solid">
    <glyph-mesh geometry="cuboctahedron"></glyph-mesh>
    <glyph-orbit-controls drag wheel></glyph-orbit-controls>
  </glyph-scene>
</glyph-camera>
```

## Shadows

Shadows are opt-in. Set `shadow` on `<GlyphScene>` to enable the shadow-map pass,
then flag individual meshes with `castShadow` and/or `receiveShadow`.
A mesh with both flags self-shadows. `<GlyphGround />` defaults to `receiveShadow=true`.

```tsx
import {
  GlyphPerspectiveCamera,
  GlyphScene,
  GlyphMesh,
  GlyphGround,
} from "@glyphcss/react";

const shadow = { color: "#000000", opacity: 0.25, lift: 0.05 };
const directionalLight = { direction: [0.5, 0.7, 0.5], intensity: 1 };

export function ShadowDemo() {
  return (
    <GlyphPerspectiveCamera rotX={45} rotY={30} zoom={50} distance={5}>
      <GlyphScene mode="solid" cols={100} rows={30}
        directionalLight={directionalLight}
        shadow={shadow}
      >
        <GlyphMesh geometry="dodecahedron" color="#4488ff"
          castShadow
          receiveShadow
        />
        <GlyphGround />
      </GlyphScene>
    </GlyphPerspectiveCamera>
  );
}
```

  

```vue
<template>
  <GlyphPerspectiveCamera :rot-x="45" :rot-y="30" :zoom="50" :distance="5">
    <GlyphScene mode="solid" :cols="100" :rows="30"
      :directional-light="directionalLight"
      :shadow="shadow"
    >
      <GlyphMesh geometry="dodecahedron" color="#4488ff"
        cast-shadow
        receive-shadow
      />
      <GlyphGround />
    </GlyphScene>
  </GlyphPerspectiveCamera>
</template>

<script setup lang="ts">
import {
  GlyphPerspectiveCamera,
  GlyphScene,
  GlyphMesh,
  GlyphGround,
} from "@glyphcss/vue";

const shadow = { color: "#000000", opacity: 0.25, lift: 0.05 };
const directionalLight = { direction: [0.5, 0.7, 0.5], intensity: 1 };
</script>
```

  

```html
<glyph-perspective-camera rot-x="45" rot-y="30" zoom="50" distance="5">
  <glyph-scene mode="solid" cols="100" rows="30"
    directional-intensity="1"
    shadow shadow-color="#000000" shadow-opacity="0.25" shadow-lift="0.05"
  >
    <glyph-mesh geometry="dodecahedron" color="#4488ff"
      cast-shadow receive-shadow
    ></glyph-mesh>
    <!-- There is no "ground" geometry name: `GlyphGround` is a React/Vue
         component only. From HTML, use a large flat mesh as the receiver. -->
    <glyph-mesh geometry="cube" scale="12 12 0.1" position="0 0 -2"
      receive-shadow
    ></glyph-mesh>
  </glyph-scene>
</glyph-perspective-camera>
```

### `GlyphShadowOptions` fields

| Field | Type | Default | Description |
|---|---|---|---|
| `color` | `string` | `"#000000"` | Shadow tint hex color |
| `opacity` | `number` | `0.25` | Darkness 0..1 toward `color` |
| `lift` | `number` | `0.05` | Depth bias — prevents self-shadow acne on flat lit surfaces. A WORLD-UNIT length, so the default assumes a room-scale scene; a scene whose unit is (say) an Earth radius must set its own, or every shadow is biased away |
| `maxExtend` | `number` | `2000` | **Accepted but not read.** The light-space volume is fitted to the casters' own bounds; this value has no effect on any render |

## Lifecycle methods (vanilla)

| Method | Description |
|---|---|
| `scene.add(polygons, transform?)` | Register a mesh, returns a `GlyphMeshHandle` |
| `mesh.setPolygons(polygons)` | Replace a mesh's geometry in place |
| `mesh.setTransform(transform)` | Replace a mesh's transform |
| `mesh.dispose()` | Remove a mesh |
| `scene.addHotspot(opts, onClick?)` | Register a hotspot overlay, returns a `GlyphHotspotHandle` |
| `hotspot.setAt(at)` | Move a hotspot's 3D anchor without touching its element — the element, its listeners and anything you wrote on it survive, and only the projected position changes |
| `hotspot.remove()` | Remove a hotspot overlay |
| `scene.setOptions(partial)` | Update any scene option and trigger a re-render |
| `scene.getOptions()` | Return a snapshot of current options |
| `scene.rerender()` | Force an immediate re-rasterize |
| `scene.addEffectLayer(opts)` | Mount an ordered appearance program over the retained grid, returns a `GlyphEffectLayerHandle` (see [Glyph Effects](/guides/effects)). Changing the handle's `params` / `opacity` / `blend` / `order` / `enabled` recomposes without re-projecting the mesh |
| `scene.setInteracting(active)` | Tell the scene a gesture is in progress, so `interactiveDownscale` applies. The bundled controls call this for you — use it for custom interaction sources |
| `scene.getOpaqueCoverage()` | The last committed render's opaque per-cell coverage at the output grid — cells owned by the base grid or an opaque detail layer, foreign stamps excluded. `null` when that render built no occlusion id-map (see `trackOpaqueCoverage`). `solid` mode only |
| `scene.setForeignOcclusion(coverage)` | Consume another scene's `getOpaqueCoverage()` result: every layer of this scene blanks under the covered cells, so two scenes stacked over one host share a single occlusion domain. `null` clears. Re-publishing identical bytes costs a comparison, not a render. `solid` mode only |
| `scene.getGlyphSemanticCellFrame()` | Immutable snapshot of the last committed base grid's polygon → surface → instance → class lineage. `null` unless `glyphOutput: "semantic"` |
| `scene.destroy()` | Remove the scene DOM and clear all meshes |
