Categories: CSSScriptWeb Design

Build 3D Voxel Art in SVG with heerich.js

heerich.js is a JavaScript voxel rendering engine that constructs 3D scenes and outputs them as SVG markup.

It represents geometry as a sparse integer voxel grid and exposes CSG-style boolean operations (union, subtract, intersect, and exclude) for additive and subtractive shape construction.

Two camera modes convert the 3D grid into flat screen coordinates, and a painter’s algorithm sorts visible faces by depth before writing the final polygon elements.

The library takes its name from Erwin Heerich, the German sculptor known for geometric cardboard constructions. His additive and subtractive method maps directly onto voxel art.

Features:

  • Render voxel scenes as plain SVG markup.
  • Build geometry with boxes, spheres, lines, and fill tests.
  • Combine shapes with union, subtract, intersect, and exclude modes.
  • Style each visible face separately.
  • Restyle existing voxels after scene creation.
  • Switch between oblique and perspective projection.
  • Rotate geometry in 90 degree turns.
  • Scale voxels per axis for partial cell fills.
  • Inject custom SVG content into voxel positions.
  • Read projected faces for custom canvas or SVG renderers.
  • Query voxels and neighbor relationships from the scene grid.
  • Serialize scene data to JSON and restore it later.

Use Cases:

  • Generate SVG based voxel art for landing pages and creative coding demos.
  • Build architectural massing studies with boolean cuts and face styling.
  • Create technical diagrams that need clean vector export and DOM access.
  • Prototype isometric editors that need direct voxel queries and custom metadata.

How To Use:

Installation

npm install heerich
import { Heerich } from 'heerich'

The UMD build also works directly in the browser. Include it via a <script> tag and the global Heerich constructor becomes available immediately.

<script src="heerich.umd.js"></script>

Basic Setup

Create a Heerich instance with a tile size and a camera configuration. The tile option sets the pixel footprint of each voxel in screen space.

import { Heerich } from 'heerich'

// Create a new scene with 40px tiles and an oblique camera at 45 degrees
const scene = new Heerich({
  tile: 40,
  camera: { type: 'oblique', angle: 45, distance: 15 },
})

Camera Modes

heerich.js supports two projection modes. Oblique projection produces the classic pixel-art isometric look. Perspective projection converges lines toward a vanishing point.

// Oblique projection — classic voxel art style
const scene = new Heerich({
  camera: { type: 'oblique', angle: 45, distance: 15 }
})

// Perspective projection — vanishing-point rendering with a configurable eye position
const scene = new Heerich({
  camera: { type: 'perspective', position: [4, 4], distance: 12 }
})

// Update the camera on an existing instance at any time
scene.setCamera({ angle: 30, distance: 18 })

Adding and Removing Geometry

applyGeometry() is the core method. Pass a shape type, a position, and an optional mode to control how voxels interact with the existing grid. The default mode is 'union'.

// Add a rectangular building base
scene.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: [6, 5, 6],
  style: {
    default: { fill: '#c8b89a', stroke: '#444' },
    top:     { fill: '#b05040' },
  }
})

// Carve a window opening and color the exposed interior walls
scene.removeGeometry({
  type: 'box',
  position: [1, 1, 0],
  size: [2, 2, 1],
  style: { default: { fill: '#1a1a2e' } }
})

// addGeometry and removeGeometry are convenience shortcuts
scene.addGeometry({ type: 'box', position: [0, 0, 0], size: 3 })
scene.removeGeometry({ type: 'box', position: [1, 1, 1], size: 1 })

Shape Types

Box

// A 4×3×4 solid block placed at the origin
scene.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: [4, 3, 4]
})

// Center-positioned variant — the engine converts center to min-corner automatically
scene.applyGeometry({
  type: 'box',
  center: [5, 5, 5],
  size: 5
})

Sphere

// A solid sphere centered at [4, 4, 4] with radius 3
scene.applyGeometry({
  type: 'sphere',
  center: [4, 4, 4],
  radius: 3,
  style: { default: { fill: '#4488cc', stroke: '#224' } }
})

// Hollow out the interior to create a shell
scene.removeGeometry({
  type: 'sphere',
  center: [4, 4, 4],
  radius: 2,
  style: { default: { fill: '#112244' } } // Color the carved interior walls
})

Line

Lines use from/to positioning. The radius and shape options control line thickness and end-cap style.

// A thin line segment along the X axis
scene.applyGeometry({
  type: 'line',
  from: [0, 0, 0],
  to: [8, 0, 0]
})

// A thick rounded column along the Y axis
scene.applyGeometry({
  type: 'line',
  from: [0, 0, 0],
  to: [0, 8, 0],
  radius: 2,
  shape: 'rounded'
})

// A thick square-capped beam along the Z axis
scene.applyGeometry({
  type: 'line',
  from: [0, 0, 0],
  to: [0, 0, 8],
  radius: 1,
  shape: 'square'
})

Custom Fill

The fill type accepts a bounds array and a test function. The engine calls test(x, y, z) for every integer coordinate inside the bounding box and includes the voxel when the function returns true. This is the general-purpose primitive behind all other shape types.

// A hollow sphere shell — ring between two radii
scene.applyGeometry({
  type: 'fill',
  bounds: [[-5, -5, -5], [5, 5, 5]],
  test: (x, y, z) => {
    const d = x * x + y * y + z * z
    return d <= 20 && d >= 12
  }
})

// A torus with major radius 6 and tube radius 2
scene.applyGeometry({
  type: 'fill',
  bounds: [[-8, -3, -8], [8, 3, 8]],
  test: (x, y, z) => {
    const R = 6, r = 2
    const q = Math.sqrt(x * x + z * z) - R
    return q * q + y * y <= r * r
  }
})

Boolean Operations

Pass mode to control how new geometry interacts with the existing voxel grid.

// Union — add voxels (default behavior)
scene.applyGeometry({ type: 'box', position: [0, 0, 0], size: 6 })

// Subtract — carve out the overlapping voxels
scene.applyGeometry({
  type: 'sphere',
  center: [3, 3, 3],
  radius: 3,
  mode: 'subtract'
})

// Intersect — keep only voxels present in both the existing grid and the new shape
scene.applyGeometry({
  type: 'box',
  position: [1, 1, 1],
  size: 4,
  mode: 'intersect'
})

// Exclude — XOR: add where empty, remove where occupied
scene.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 6,
  mode: 'exclude'
})

Styling

Set SVG presentation attributes on up to six named face directions. The default key acts as a fallback for any face not explicitly listed.

scene.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 4,
  style: {
    default: { fill: '#5577aa', stroke: '#223' },
    top:     { fill: '#7799cc' },
    front:   { fill: '#446688' },
  }
})

Style values also accept functions of (x, y, z) for position-driven color:

scene.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 10,
  style: {
    default: (x, y, z) => ({
      fill: `hsl(${x * 36}, 55%, ${45 + z * 4}%)`,
      stroke: '#111',
    })
  }
})

Restyle existing voxels at any time:

// Change only the top face color — geometry is untouched
scene.applyStyle({
  type: 'box',
  position: [0, 0, 0],
  size: 4,
  style: { top: { fill: '#ff4444' } }
})

Voxel Scaling

Scale individual voxels along any axis. Scaled voxels become non-opaque automatically, revealing the geometry behind them. The scaleOrigin option sets the anchor point within the voxel cell (0–1 per axis).

// Static scale — flatten all voxels to half height, pinned at the bottom
scene.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 4,
  scale: [1, 0.5, 1],
  scaleOrigin: [0.5, 1, 0.5] // Bottom-center anchor
})

// Functional scale — taper a column by vertical position
scene.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 6,
  scale: (x, y, z) => [1, 1 - y * 0.15, 1], // Narrower toward the top
  scaleOrigin: [0.5, 1, 0.5]
})

Rotation

Rotate geometry by 90-degree increments on any axis. Apply rotation to a shape before placement, or rotate the full scene in place.

// Rotate a shape before adding it to the scene
scene.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: [6, 1, 4],
  rotate: { axis: 'z', turns: 1 } // 90 degrees around Z
})

// Rotate all existing voxels 180 degrees around Y
scene.rotate({ axis: 'y', turns: 2 })

// Rotate around an explicit center coordinate
scene.rotate({ axis: 'x', turns: 1, center: [4, 4, 4] })

Rendering to SVG

Call toSVG() to produce the final SVG string. Pass options to control padding, viewBox, and injected SVG content.

// Basic render — write directly into the document
document.body.innerHTML = scene.toSVG()

// Add padding around the viewBox
const svg = scene.toSVG({ padding: 30 })

// Override the viewBox entirely
const svg = scene.toSVG({ viewBox: [0, 0, 800, 600] })

// Inject SVG filters for a cel-shaded outline effect
const svg = scene.toSVG({
  prepend: `<defs><filter id="outline">
    <feMorphology in="SourceAlpha" operator="dilate" radius="2" result="expanded"/>
    <feFlood flood-color="#000"/>
    <feComposite in2="expanded" operator="in" result="border"/>
    <feMerge><feMergeNode in="border"/><feMergeNode in="SourceGraphic"/></feMerge>
  </filter></defs><g filter="url(#outline)">`,
  append: `</g>`,
})

Every rendered polygon carries data attributes:

<polygon data-voxel="2,3,1" data-x="2" data-y="3" data-z="1" data-face="top" ... />

Custom Renderers

getFaces() returns the full projected face array for use in Canvas or any other renderer:

const faces = scene.getFaces()

const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')

for (const face of faces) {
  if (face.type === 'content') continue

  const d = face.points.data // [x0, y0, x1, y1, x2, y2, x3, y3] — four quad corners
  ctx.beginPath()
  ctx.moveTo(d[0], d[1])
  ctx.lineTo(d[2], d[3])
  ctx.lineTo(d[4], d[5])
  ctx.lineTo(d[6], d[7])
  ctx.closePath()
  ctx.fillStyle = face.style.fill
  ctx.fill()
}

Use renderTest() for a stateless render pass. The engine runs the test function, returns faces, and stores nothing.

// Stateless render — no voxels written to the scene
const faces = scene.renderTest({
  bounds: [[-6, -6, -6], [6, 6, 6]],
  test: (x, y, z) => x * x + y * y + z * z <= 30,
  style: (x, y, z, faceName) => ({ fill: faceName === 'top' ? '#ddd' : '#aaa' })
})

// Pass pre-computed faces directly to toSVG
const svg = scene.toSVG({ faces })

Content Voxels

Embed arbitrary SVG at a voxel position. Content voxels depth-sort with the rest of the scene.

// Place an SVG text label at a specific grid coordinate
scene.applyGeometry({
  type: 'box',
  position: [2, 0, 2],
  size: 1,
  content: '<text font-size="10" text-anchor="middle">A1</text>',
  opaque: false,
})

Querying and Serialization

// Check whether a voxel exists at a coordinate
const exists = scene.hasVoxel([3, 2, 1])  // true or false

// Get full voxel data, or null if the cell is empty
const voxel = scene.getVoxel([3, 2, 1])

// Get all six neighbors by face name
const neighbors = scene.getNeighbors([3, 2, 1])
// Returns: { top, bottom, left, right, front, back }

// Iterate over every voxel in the scene
for (const voxel of scene) {
  console.log(voxel.x, voxel.y, voxel.z, voxel.styles)
}

// Serialize the full grid to a plain object
const snapshot = JSON.stringify(scene.toJSON())

// Restore from serialized data
const restored = Heerich.fromJSON(JSON.parse(snapshot))

Note: functional style callbacks are not serializable. The engine logs a console warning and omits them. Store callback logic in your application code and reapply it after restoring from fromJSON().

Configuration Options

  • tile (number): Pixel size of each voxel in screen space.
  • camera (object): Camera configuration. Accepts type ('oblique' or 'perspective'), angle, distance, and position.
  • mode (string): Boolean operation for applyGeometry. Accepts 'union', 'subtract', 'intersect', or 'exclude'. Defaults to 'union'.
  • style (object or function): Per-face style map. Keys are face names (default, top, bottom, left, right, front, back). Values are SVG attribute objects or functions of (x, y, z).
  • content (string): Raw SVG string placed at the voxel position instead of polygon faces.
  • opaque (boolean): Sets voxel occlusion behavior. A value of false lets neighboring voxels show through. Defaults to true. The engine sets this to false automatically on scaled voxels.
  • meta (object): Key/value pairs emitted as data-* attributes on the voxel’s SVG polygons.
  • rotate (object): Rotation applied to the shape before placement. Accepts axis ('x', 'y', or 'z') and turns (number of 90-degree increments).
  • scale (array or function): Per-axis scale factor from 0–1. Accepts [sx, sy, sz] or a function of (x, y, z) returning the scale array. Return null from the function to leave a specific voxel at full size.
  • scaleOrigin (array or function): Anchor point for scaling within the voxel cell (0–1 per axis). Defaults to [0.5, 0, 0.5]. Accepts [ox, oy, oz] or a function of (x, y, z).
  • bounds (array): Bounding box for fill shapes. Format: [[minX, minY, minZ], [maxX, maxY, maxZ]].
  • test (function): Voxel inclusion test for fill shapes. The engine calls it with (x, y, z) and includes the voxel when the function returns true.
  • position (array): Min-corner coordinate for box, sphere, and fill shapes. Format: [x, y, z].
  • center (array): Geometric center coordinate. The engine converts this to position automatically based on the shape’s size.
  • size (number or array): Dimensions for box shapes. Accepts a single number for a uniform cube or [w, h, d] for a rectangular block.
  • radius (number): Radius for sphere and line shapes.
  • from / to (array): Start and end coordinates for line shapes.
  • shape (string): End-cap style for thick lines. Accepts 'rounded' or 'square'.
  • padding (number): ViewBox padding in pixels for toSVG(). Defaults to 20.
  • viewBox (array): Custom viewBox override for toSVG(). Format: [x, y, w, h].
  • prepend / append (string): Raw SVG inserted before or after the face polygons in toSVG() output.
  • faceAttributes (function): Per-face attribute callback for toSVG(). Receives each face and returns additional SVG attributes.

API Methods

// Apply a boolean operation to the voxel grid
scene.applyGeometry(opts)

// Add voxels using union mode — shortcut for applyGeometry with mode: 'union'
scene.addGeometry(opts)

// Remove voxels using subtract mode — shortcut for applyGeometry with mode: 'subtract'
scene.removeGeometry(opts)

// Restyle existing voxels without modifying geometry
scene.applyStyle(opts)

// Update camera settings on the existing instance
scene.setCamera(cameraOpts)

// Rotate all voxels in the scene by 90-degree increments
scene.rotate({ axis: 'y', turns: 1 })

// Render the scene to an SVG string
const svg = scene.toSVG(options)

// Get the projected 2D face array for custom renderers
const faces = scene.getFaces()

// Stateless render — returns faces from a test function, no voxels stored
const faces = scene.renderTest({ bounds, test, style })

// Compute the 2D bounding box of the rendered geometry
const { x, y, w, h } = scene.getBounds(padding)

// Return true if a voxel exists at the given coordinate
const exists = scene.hasVoxel([2, 3, 1])

// Return voxel data at a coordinate, or null if the cell is empty
const voxel = scene.getVoxel([2, 3, 1])

// Return an object with the six neighboring voxels keyed by face name
const neighbors = scene.getNeighbors([2, 3, 1])

// Serialize the voxel grid to a plain object
const data = scene.toJSON()

// Restore a scene from a previously serialized object (static method)
const restored = Heerich.fromJSON(data)

FAQs:

Q: My voxels appear in the wrong vertical position. What’s happening?
A: The Y axis points downward, following SVG screen-space convention. A voxel at y: -4 renders above the origin, and one at y: 4 renders below it. Negate your Y values when porting coordinates from a standard mathematical 3D system.

Q: How do I attach click events to specific voxels after rendering?
A: Each rendered polygon carries data-x, data-y, data-z, and data-face attributes. Query the SVG for them directly: document.querySelectorAll('[data-face="top"]') returns every top-face polygon. Attach standard DOM event listeners to those elements.

Q: Can I animate a heerich.js scene?
A: Call applyGeometry(), applyStyle(), or setCamera() to modify the scene state, then call toSVG() again and write the result back into the DOM. Run this cycle inside requestAnimationFrame for smooth playback. Functional styles that reference a time variable re-evaluate on every toSVG() call.

The post Build 3D Voxel Art in SVG with heerich.js appeared first on CSS Script.

rssfeeds-admin

Share
Published by
rssfeeds-admin

Recent Posts

Generate A Clean Calendar For Any Month And Year – Calendar.js

Calendar.js is a tiny JavaScript library for generating a calendar UI based on the year…

2 hours ago

North Korean IT Worker Accused Of Using Stolen Identity For Job Scam

In June 2025, cybersecurity firm Nisos uncovered a sophisticated employment fraud scheme when a suspected…

2 hours ago

Exposed Server Leaks The Gentlemen Ransomware Toolkit and Stolen Credentials

A newly discovered exposed server has revealed critical insights into the operations of the TheGentlemen…

2 hours ago

GhostSocks Malware Converts Victim Systems Into Residential Proxies

In today’s threat landscape, blending into normal network activity is crucial for cybercriminals. Threat actors…

2 hours ago

Vim Modeline Bypass Vulnerability Let Attackers Execute Arbitrary OS Commands

A newly discovered high-severity vulnerability in the popular Vim text editor exposes users to arbitrary…

2 hours ago

Public PoC Exploit Released for Nginx-UI Backup Restore Vulnerability

A critical security flaw has been disclosed in the Nginx-UI backup restore mechanism, tracked as…

2 hours ago

This website uses cookies.