Render Modes
glyphcss has four render modes. Each maps the same projected mesh to a different glyph layout.
| Mode | Best for | Cost |
|---|---|---|
wireframe | Geometric meshes, lattices, line art | Scales with edge count |
solid | Smooth-shaded surfaces from real mesh files | Scales with triangle count × grid cells |
voxel | MagicaVoxel .vox files, chunky low-poly | Scales with visible voxel faces |
ink | Illustration-style outlines (silhouette + crease), sphere/torus contours | Scales with triangle-edge count |
wireframe
Section titled “wireframe”Bresenham-rasterizes each edge into a Uint8Array stamp with three weight tiers,
then maps the stamp to glyphs:
| Weight | Glyph palette | Used for |
|---|---|---|
| 1 (thin) | ·⋅∙˙·⋅∙ | Spokes, inner lattice, decorative scaffolding |
| 2 (normal) | ╋╬┼╳◆◇◊▲△▼▽◈⬡⬢∴∵⊥⊕⊗⊙⊚⊛ | Main cage edges |
| 3 (core) | ✦✧✩◉⊙◎ | Focal accents, “sun” centre |
import { GlyphCamera, GlyphScene, GlyphMesh, GlyphOrbitControls } from "@glyphcss/react";import { icosahedronPolygons } from "@glyphcss/core";
const icosa = icosahedronPolygons({ center: [0, 0, 0], size: 1, color: "#44ffcc" });
export function WireframeDemo() { return ( <GlyphCamera rotX={25}> <GlyphScene mode="wireframe" cols={80} rows={24}> <GlyphOrbitControls /> <GlyphMesh polygons={icosa} /> </GlyphScene> </GlyphCamera> );}import { createGlyphCamera, createGlyphScene } from "glyphcss";
const camera = createGlyphCamera({ rotX: 25 });const scene = createGlyphScene(document.querySelector("#scene")!, { camera, mode: "wireframe", cols: 80, rows: 24,});
// Custom wireframe edges with explicit weights:import { buildRasterizeContext, rasterize } from "glyphcss";
const grid = { cols: 80, rows: 24, cellAspect: 2.0 };const ctx = buildRasterizeContext({ camera, grid, wireframe: [ { from: [1, 0, 0], to: [-1, 0, 0], weight: 2 }, // bold edge { from: [0, 1, 0], to: [0, -1, 0], weight: 1 }, // thin spoke ], mode: "wireframe",});const text = rasterize(ctx);Best for: geometric meshes, lattices, iconic visuals.
Character encoding (charMode)
Section titled “Character encoding (charMode)”charMode selects the character encoding used to draw wireframe output.
"ascii" (default) is the ramp/rule-glyph encoding above. "braille" instead
packs a 2×4 subcell dot grid into every output cell using Unicode Braille
Patterns (U+2800..U+28FF), giving up to 8 independent “pixels” per glyph —
visibly smoother diagonal and curved edges than a single ASCII rule glyph per
cell.
charMode: "braille" is a documented no-op in solid, voxel, and ink
modes: braille dot coverage is binary, so it cannot carry a Lambert shade
ramp, a voxel face-normal glyph, or ink’s oriented direction glyph. Those
modes always render ASCII regardless of this option.
<GlyphScene mode="wireframe" charMode="braille" cols={80} rows={24}> <GlyphOrbitControls /> <GlyphMesh polygons={icosa} /></GlyphScene>const scene = createGlyphScene(document.querySelector("#scene")!, { camera, mode: "wireframe", charMode: "braille", cols: 80, rows: 24,});Mirrored as char-mode="braille" on <glyph-scene> and charMode on
@glyphcss/react/@glyphcss/vue’s <GlyphScene>.
Box-drawing junctions (wireframeJunctions)
Section titled “Box-drawing junctions (wireframeJunctions)”By default, each wireframe edge picks its cell glyphs independently from the active palette (weight tier → random glyph), so two edges meeting in the same cell — a corner, a T-junction, a crossing — render whichever edge happened to rasterize last. On an axis-aligned mesh (a cube face-on, a voxel grid, an iso diagram) this visibly breaks corners and joints.
wireframeJunctions: true (default false, charMode: "ascii" only) runs a
second pass over the finished wireframe cells: every near-axis-aligned edge
accumulates which of a cell’s four sides (N/E/S/W) carry a line, and any cell
with a non-zero side mask resolves to the matching glyph from the fixed
┌┐└┘├┤┬┴┼─│ box-drawing set instead of the random pick — so the joint reads
as ONE glyph consistent with every edge touching it. An edge counts as
near-axis-aligned when its two projected endpoints round to the same output
row or column; a diagonal-dominant edge contributes nothing to the mask and
keeps the default slope-glyph behavior.
<GlyphScene mode="wireframe" wireframeJunctions cols={80} rows={24}> <GlyphOrbitControls /> <GlyphMesh polygons={cube} /></GlyphScene>const scene = createGlyphScene(document.querySelector("#scene")!, { camera, mode: "wireframe", wireframeJunctions: true, cols: 80, rows: 24,});Mirrored as wireframe-junctions on <glyph-scene> and wireframeJunctions
on @glyphcss/react/@glyphcss/vue’s <GlyphScene>.
Hidden-line removal (hiddenLines)
Section titled “Hidden-line removal (hiddenLines)”The wireframe path has no depth reference by default: edges draw in mesh
order, and a contested cell resolves by edge weight, not by which edge is
nearer the camera. A farther edge — another mesh’s far side, an extruded side
wall behind a front face — can paint over a nearer one. This shows up as
cross-object bleed-through, braille strokes mixing with a farther stroke’s
dots, or a darker sideColor edge overwriting a brighter front-face color.
hiddenLines: "hide" (default "show") depth-tests every wireframe stroke
against a solid surface prepass, with a slope-scaled bias so a smooth
surface’s own silhouette isn’t eaten by the surface it outlines. It applies to
both charMode: "ascii" and "braille", and is a documented no-op in solid
mode (already depth-buffered per cell).
ink mode also wires hiddenLines: "hide", fixing the same defect on
extruded text and other closed meshes. Its test is identity-based: a stroke is
never occluded by its own local surface neighborhood, only by genuinely
different geometry. Silhouettes survive intact, while an extrusion’s back and
side walls stop painting over its own front face — and a wall only partly
behind a cap hides just the covered portion.
<GlyphScene mode="wireframe" hiddenLines="hide" cols={80} rows={24}> <GlyphOrbitControls /> <GlyphMesh polygons={extrudedText} /></GlyphScene>const scene = createGlyphScene(document.querySelector("#scene")!, { camera, mode: "wireframe", hiddenLines: "hide", cols: 80, rows: 24,});Mirrored as hidden-lines on <glyph-scene> and hiddenLines on
@glyphcss/react/@glyphcss/vue’s <GlyphScene>. Also accepted by
compileScene/GlyphSceneStatic (unlike wireframeJunctions, which is
runtime-only) — it’s a pure function of geometry and camera, exactly like
charMode, and fixes a genuine occlusion defect a static bake shouldn’t
reproduce.
Triangle scan-fill, with Lambert shading per cell mapped to a ramp:
" .:-=+*#%@"Darker glyphs for cells facing away from the directional light, brighter glyphs
for cells facing it. Shading is flat by default — one normal per polygon, so
facets stay visible. Set smoothShading (with creaseAngle, default 60°) to
interpolate per-cell normals from averaged vertex normals instead.
import { GlyphCamera, GlyphScene, GlyphMesh, GlyphOrbitControls } from "@glyphcss/react";import { dodecahedronPolygons } from "@glyphcss/core";
const dodeca = dodecahedronPolygons({ center: [0, 0, 0], size: 1, color: "#cc44ff" });
export function SolidDemo() { return ( <GlyphCamera rotX={25}> <GlyphScene mode="solid" cols={100} rows={30} directionalLight={{ direction: [0.5, 0.7, 0.5], intensity: 1 }} ambientLight={{ intensity: 0.4 }} > <GlyphOrbitControls /> <GlyphMesh polygons={dodeca} /> </GlyphScene> </GlyphCamera> );}import { createGlyphCamera, createGlyphScene } from "glyphcss";import { dodecahedronPolygons } from "@glyphcss/core";
const camera = createGlyphCamera({ rotX: 25 });const scene = createGlyphScene(document.querySelector("#scene")!, { camera, mode: "solid", cols: 100, rows: 30, directionalLight: { direction: [0.5, 0.7, 0.5], intensity: 1 }, ambientLight: { intensity: 0.4 },});scene.add(dodecahedronPolygons({ center: [0, 0, 0], size: 1, color: "#cc44ff" }));Best for: smooth-shaded surfaces from real mesh files.
Two-color cells (charMode: "halfblock")
Section titled “Two-color cells (charMode: "halfblock")”charMode: "halfblock" is the solid-mode mirror of wireframe’s "braille":
braille buys SHAPE resolution at one color per cell, halfblock buys COLOR
resolution at coarser (block) shape. Instead of picking one glyph from the
shade ramp per cell, it packs two independently colored subcells — top and
bottom — into a single cell using ▀/▄/█, for 2× vertical color
resolution. Because a monospace cell is already roughly twice as tall as it is
wide, each half lands close to square — a better pixel grid than the ramp
glyph it replaces.
An empty cell never paints a background: █ is used only when a cell’s top
and bottom both resolve to the same color (a common case on flat-shaded
surfaces, cheaper markup than two colors), ▀/▄ when only one half is
covered, and ▀ with background-color set to the bottom color only when
both halves are covered with genuinely different colors.
<GlyphScene mode="solid" charMode="halfblock" cols={80} rows={24}> <GlyphOrbitControls /> <GlyphMesh polygons={dodeca} /></GlyphScene>const scene = createGlyphScene(document.querySelector("#scene")!, { camera, mode: "solid", charMode: "halfblock", cols: 80, rows: 24,});charMode: "halfblock" is a documented no-op outside solid mode, and also a
no-op when combined with a transformCells hook or active temporalBlend
reprojection — both already expect the single-color-per-cell output. Mirrored
as char-mode="halfblock" on <glyph-scene> and charMode on
@glyphcss/react/@glyphcss/vue’s <GlyphScene>.
Four-quadrant cells (charMode: "quadrant")
Section titled “Four-quadrant cells (charMode: "quadrant")”charMode: "quadrant" generalizes "halfblock" from a 1×2 (top/bottom)
subcell split to a full 2×2 split — TL/TR/BL/BR — buying BOTH shape and color
resolution at the same two-colors-per-cell markup cost. It picks from all 16
Unicode quadrant/half/full-block glyphs (space, ▘▝▖▗▀▄▌▐▚▞█, and the
three-quadrant glyphs ▛▜▙▟), which cover every possible binary 2×2 coverage
pattern exactly — halfblock’s ▀/▄ are just two of these 16 masks.
A partially-covered cell (1-3 of the 4 regions) renders the exact coverage
mask — 14 distinct partial silhouettes, instead of halfblock’s fixed
top/bottom rounding — with one averaged color and no background (a background
would have to paint an uncovered region, which never happens). A
fully-covered cell either collapses to a single-color █ (same color on all
four regions) or splits into a genuine two-tone glyph: the 4 regions partition
into a “high”/“low” luminance group against their mean, and the matching mask
renders with color = the high group and background-color = the low group.
<GlyphScene mode="solid" charMode="quadrant" cols={80} rows={24}> <GlyphOrbitControls /> <GlyphMesh polygons={dodeca} /></GlyphScene>const scene = createGlyphScene(document.querySelector("#scene")!, { camera, mode: "solid", charMode: "quadrant", cols: 80, rows: 24,});charMode: "quadrant" shares every eligibility rule "halfblock" has: a
documented no-op outside solid mode, and no-op when combined with a
transformCells hook or active temporalBlend reprojection. Mirrored as
char-mode="quadrant" on <glyph-scene> and charMode on
@glyphcss/react/@glyphcss/vue’s <GlyphScene>.
Font-weight density ramp (solidWeightRamp)
Section titled “Font-weight density ramp (solidWeightRamp)”solidWeightRamp adds a second density axis to solid shading: CSS
font-weight, alongside the usual glyph-shape ramp. Bold does not change
monospace advance width (verified across common stacks, including
browser-synthesized bold), so a weight-bearing span can never desync the
character grid.
The ramp is measurement data, not a plain string — a list of
(glyph, weight) steps ordered darkest → densest by real ink coverage,
produced by @glyphcss/effects’s calibrateWeightedGlyphRamp:
import { calibrateWeightedGlyphRamp } from "@glyphcss/effects";
const { steps } = calibrateWeightedGlyphRamp({ font: { family: "ui-monospace, monospace", size: 32 }, steps: 24, weights: [400, 700],});const solidWeightRamp = steps.map(({ glyph, weight }) => ({ glyph, weight: Number(weight) }));When set, solidWeightRamp replaces glyphPalette’s solid ramp —
shading picks both a glyph and a weight from the calibrated steps, buying
more distinguishable shading buckets and a darker dark end than glyph shape
alone provides.
<GlyphScene mode="solid" solidWeightRamp={solidWeightRamp} cols={80} rows={24}> <GlyphOrbitControls /> <GlyphMesh polygons={dodeca} /></GlyphScene>const scene = createGlyphScene(document.querySelector("#scene")!, { camera, mode: "solid", solidWeightRamp, cols: 80, rows: 24,});undefined (the default) leaves the render byte-identical. Documented no-op
outside solid mode, under charMode: "halfblock" or "quadrant" (their
two-color encodings have no font-weight span), and during active
temporalBlend reprojection. Mirrored as a solidWeightRamp JS property on
<glyph-scene> (the ramp is data, not an attribute) and solidWeightRamp on
@glyphcss/react/@glyphcss/vue’s <GlyphScene>. Also accepted by
compileScene — the ramp is plain step data, so a weighted-ramp render bakes
at build time too.
One glyph per voxel face, depth-sorted. Natural fit for MagicaVoxel .vox files
where the source data is already cell-aligned.
import { GlyphCamera, GlyphScene, GlyphMesh, GlyphOrbitControls } from "@glyphcss/react";import type { Polygon } from "@glyphcss/core";
// `polygons` comes from parseVox() — load the file outside the component.// Replace "/tree.vox" with your own MagicaVoxel file path.export function VoxelDemo({ polygons }: { polygons: Polygon[] }) { return ( <GlyphCamera rotX={25}> <GlyphScene mode="voxel" cols={80} rows={24}> <GlyphOrbitControls /> <GlyphMesh polygons={polygons} /> </GlyphScene> </GlyphCamera> );}import { createGlyphCamera, createGlyphScene } from "glyphcss";import { parseVox } from "@glyphcss/core";
// Replace "/tree.vox" with the path to your own MagicaVoxel file.const voxBuffer = await fetch("/tree.vox").then((r) => r.arrayBuffer());const { polygons } = parseVox(voxBuffer);
const camera = createGlyphCamera({ rotX: 25 });const scene = createGlyphScene(document.querySelector("#scene")!, { camera, mode: "voxel", cols: 80, rows: 24,});scene.add(polygons);Today voxel routes through the wireframe rasterizer rather than owning a
separate path, so it renders cell-aligned geometry as edges. A dedicated
voxel color → glyph-density mapping does not exist yet.
Best for: voxel art, chunky low-poly aesthetics.
Oriented silhouette + crease outline mode: detects view-dependent silhouette
edges (front-facing/back-facing sign flip across a shared triangle edge) and
fixed dihedral-angle crease edges, chains them into contours, smooths the
screen-space tangent along each chain, and picks a glyph — _ / | \ - ‾ ▏ ▕ — that
traces the local contour direction. Interior cells stay empty; hatching and
texture fills are a future effect-layer concern, not baked into this mode.
import { GlyphCamera, GlyphScene, GlyphMesh, GlyphOrbitControls } from "@glyphcss/react";import { spherePolygons } from "@glyphcss/core";
const sphere = spherePolygons({ center: [0, 0, 0], size: 1, color: "#44ffcc" });
export function InkDemo() { return ( <GlyphCamera rotX={25}> <GlyphScene mode="ink" cols={80} rows={24}> <GlyphOrbitControls /> <GlyphMesh polygons={sphere} /> </GlyphScene> </GlyphCamera> );}import { createGlyphCamera, createGlyphScene } from "glyphcss";import { spherePolygons } from "@glyphcss/core";
const camera = createGlyphCamera({ rotX: 25 });const scene = createGlyphScene(document.querySelector("#scene")!, { camera, mode: "ink", cols: 80, rows: 24,});scene.add(spherePolygons({ center: [0, 0, 0], size: 1, color: "#44ffcc" }));charMode, wireframeJunctions, and solidWeightRamp are documented no-ops
in ink mode — it always picks its own fixed oriented-glyph set as plain
ASCII (and has no shade ramp for solidWeightRamp to extend). hiddenLines
IS wired in ink — see Hidden-line removal
above.
Best for: illustration-style previews, product-shot outlines, sphere/torus/organic silhouettes where a filled ramp or a dense wireframe cage reads as noise.
Shadows
Section titled “Shadows”Shadows use a shadow-map technique: the rasterizer renders a depth pass from the
light’s perspective, then compares each cell’s depth against it. Set shadow on
<GlyphScene> to enable, then opt individual meshes in with castShadow and
receiveShadow. A mesh with both flags self-shadows. <GlyphGround> defaults to
receiveShadow=true.
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 };const ambientLight = { intensity: 0.35 };
export function ShadowDemo() { return ( <GlyphPerspectiveCamera rotX={45} rotY={30} zoom={50} distance={5}> <GlyphScene mode="solid" cols={100} rows={30} directionalLight={directionalLight} ambientLight={ambientLight} shadow={shadow} > <GlyphMesh geometry="dodecahedron" color="#4488ff" castShadow receiveShadow /> <GlyphGround /> </GlyphScene> </GlyphPerspectiveCamera> );}import { createGlyphPerspectiveCamera, createGlyphScene } from "glyphcss";import { dodecahedronPolygons, planePolygons } from "@glyphcss/core";
const camera = createGlyphPerspectiveCamera({ rotX: 45, rotY: 30, zoom: 50, distance: 5 });const scene = createGlyphScene(document.querySelector("#scene")!, { camera, mode: "solid", cols: 100, rows: 30, directionalLight: { direction: [0.5, 0.7, 0.5], intensity: 1 }, ambientLight: { intensity: 0.35 }, shadow: { color: "#000000", opacity: 0.25, lift: 0.05 },});
const caster = scene.add( dodecahedronPolygons({ center: [0, 0, 0], size: 1, color: "#4488ff" }), { castShadow: true, receiveShadow: true },);
const ground = scene.add( planePolygons({ axis: 1, size: 5, offset: 0, color: "#444444" }), { position: [0, -0.5, 0], receiveShadow: true },);Shadow fields (GlyphShadowOptions):
| Field | Default | Description |
|---|---|---|
color | "#000000" | Shadow tint color |
opacity | 0.25 | Darkness 0..1 |
lift | 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 | 2000 | Accepted but not read. The light-space volume is fitted to the casters’ own bounds; this value has no effect on any render |
Built-in geometry attribute
Section titled “Built-in geometry attribute”The <glyph-mesh> custom element and the GlyphMesh component accept a
geometry string shortcut for any name in GlyphGeometryName — a public
registry of ~44 shapes in @glyphcss/core (every Platonic, Archimedean and
Catalan solid, plus sphere, cylinder, cone, torus, pyramid, prism
and more), resolved by the exported resolveGeometry. An unknown name throws
Unknown geometry. A few of the most-used values:
| Value | Shape | Notes |
|---|---|---|
"cuboctahedron" | Cuboctahedron | Default demo shape |
"icosahedron" | Icosahedron, edge-only wireframe | 20 faces |
"cube" | Plain cube wireframe | 6 faces |
// Built-in preset (no import needed):<GlyphMesh geometry="cuboctahedron" />
// Equivalent procedural form (more control):import { icosahedronPolygons } from "@glyphcss/core";<GlyphMesh polygons={icosahedronPolygons({ center: [0,0,0], size:1 })} />