Skip to content

Render Modes

glyphcss has four render modes. Each maps the same projected mesh to a different glyph layout.

ModeBest forCost
wireframeGeometric meshes, lattices, line artScales with edge count
solidSmooth-shaded surfaces from real mesh filesScales with triangle count × grid cells
voxelMagicaVoxel .vox files, chunky low-polyScales with visible voxel faces
inkIllustration-style outlines (silhouette + crease), sphere/torus contoursScales with triangle-edge count

Bresenham-rasterizes each edge into a Uint8Array stamp with three weight tiers, then maps the stamp to glyphs:

WeightGlyph paletteUsed 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>
);
}

Best for: geometric meshes, lattices, iconic visuals.

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>

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>

Mirrored as wireframe-junctions on <glyph-scene> and wireframeJunctions on @glyphcss/react/@glyphcss/vue’s <GlyphScene>.

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>

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>
);
}

Best for: smooth-shaded surfaces from real mesh files.

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>

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>

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>

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>
);
}

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>
);
}

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 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>
);
}

Shadow fields (GlyphShadowOptions):

FieldDefaultDescription
color"#000000"Shadow tint color
opacity0.25Darkness 0..1
lift0.05Depth 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
maxExtend2000Accepted but not read. The light-space volume is fitted to the casters’ own bounds; this value has no effect on any render

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:

ValueShapeNotes
"cuboctahedron"CuboctahedronDefault demo shape
"icosahedron"Icosahedron, edge-only wireframe20 faces
"cube"Plain cube wireframe6 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 })} />