Lightweight 3D engine that uses SVG as a rendering environment.
Used technologies:
- TypeScript as the main language
- Vite for the dev server and library build
- Vitest for unit tests
- Oxlint and Oxfmt for linting and formatting
- pnpm as the package manager
You can link to the 3D SVG engine files hosted online:
<script src="https://cdn.jsdelivr.net/npm/3d-svg-engine@0.1.0/dist/3dengine.dist.js"></script>A sample HTML page might look like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Starter template</title>
</head>
<body>
<div id="world" style="width: 800px; height: 600px"></div>
<script src="https://cdn.jsdelivr.net/npm/3d-svg-engine@0.1.0/dist/3dengine.dist.js"></script>
<script>
// create global object
const engine = SVGEngine('world')
// move the camera back a little
engine.player.position = { x: 0, y: 0, z: -50 }
// add cube on scene
engine.add(engine.cube(10))
</script>
</body>
</html>Click on the root element to capture the pointer. While the pointer is captured you can rotate the camera with the mouse and use these keys:
| Key | Effect |
|---|---|
| WASD | Move camera |
| shift | Flying down |
| spacebar | Flying up |
| arrows | Rotate camera |
Live demo: flat shading, fog, neon glow, a Utah teapot loaded from .obj, animated parametric surfaces. Every frame can be saved as an editable vector .svg.
| Lighting | Synthwave |
|---|---|
![]() |
![]() |
| Teapot (.obj) | Surfaces |
![]() |
![]() |
Older examples:
Coordinates: x points right, y points up, z points away from the camera. Angles are in degrees.
Every figure accepts an optional texture — URL of an image stretched over each face.
| Param | Description | Default | Example |
|---|---|---|---|
| size | Cube side length | - | 10.23 |
| x | x coordinate of the cube center | 0 |
12 |
| y | y coordinate of the cube center | 0 |
23 |
| z | z coordinate of the cube center | 0 |
34 |
| color | Cube color | 'black' |
'#ff4433ee' |
| texture | Texture image URL | - | 'brick.png' |
| Param | Description | Default | Example |
|---|---|---|---|
| size | Pyramid side length and height | - | 23.4 |
| x | x coordinate of the pyramid center | 0 |
23.3 |
| y | y coordinate of the pyramid center | 0 |
534.2 |
| z | z coordinate of the pyramid center | 0 |
-23.3 |
| color | Pyramid color | 'black' |
'#ffee99ee' |
| texture | Texture image URL | - | 'brick.png' |
| Param | Description | Default | Example |
|---|---|---|---|
| sizea | Depth (along z) | - | 23.4 |
| sizeb | Height (along y) | - | 12.3 |
| sizec | Width (along x) | - | 10.3 |
| x | x coordinate of the parallelepiped center | 0 |
23 |
| y | y coordinate of the parallelepiped center | 0 |
2.2 |
| z | z coordinate of the parallelepiped center | 0 |
-3.2 |
| color | Parallelepiped color | 'black' |
'#ff443366' |
| texture | Texture image URL | - | 'brick.png' |
| Param | Description | Default | Example |
|---|---|---|---|
| r | Sphere radius | - | 34.4 |
| x | x coordinate of the sphere center | 0 |
23 |
| y | y coordinate of the sphere center | 0 |
23.4 |
| z | z coordinate of the sphere center | 0 |
34.5 |
| color | Sphere color | 'black' |
'#993399' |
| texture | Texture image URL | - | 'earth.png' |
| segments | Number of meridians and parallels | 10 |
18 |
Horizontal square grid made of cells about 10 units wide.
| Param | Description | Default | Example |
|---|---|---|---|
| size | Floor side length | - | 200 |
| x | x coordinate of the floor center | 0 |
0 |
| y | y coordinate of the floor center | 0 |
-10 |
| z | z coordinate of the floor center | 0 |
0 |
| color | Floor color | 'black' |
'#888' |
| texture | Texture image URL | - | 'tile.png' |
Flat square in the XY plane.
Triangle by three points, e.g. engine.triangle({ x: 0, y: 0, z: 0 }, { x: 10, y: 0, z: 0 }, { x: 0, y: 10, z: 0 }, 'red').
Grids of quads built from a function f(u, v, t). They can be animated with setTime(t) and keep any rotate / translate applied before.
| Function | Description |
|---|---|
torus(R, r, x, y, z, color, segments = 32) |
Torus: R — distance to the tube center, r — tube radius |
mobius(R, width, x, y, z, color, segments = 48) |
Möbius strip |
waves(size, height, x, y, z, color, segments = 24) |
Water surface, call setTime(seconds) every frame to animate it |
surface(fn, x, y, z, options) |
Any surface, see below |
// a "breathing" sphere
const blob = engine.surface(
(u, v, t) => {
const r = 10 + Math.sin(u * 20 + t * 3) * Math.sin(v * 10)
const a = u * Math.PI * 2
const b = v * Math.PI
return { x: r * Math.sin(b) * Math.cos(a), y: r * Math.cos(b), z: r * Math.sin(b) * Math.sin(a) }
},
0,
10,
40,
{ segmentsU: 32, segmentsV: 16, color: '#30a46c' },
)
engine.add(blob)
engine.update(() => blob.setTime(performance.now() / 1000))Options: segmentsU, segmentsV, rangeU and rangeV ([0, 1] by default), color, closed (true if the surface is closed and its normals point outward, so back faces can be skipped).
const teapot = await engine.loadObj('teapot.obj', 0, 0, 50, { size: 40, color: '#d1495b' })
engine.add(teapot)| Function | Description |
|---|---|
loadObj(url, x, y, z, options) |
Fetch a Wavefront .obj file and build a model (a Promise) |
parseObj(text, x, y, z, options) |
Build a model from .obj text |
Options: size — scale the model so its largest side has this length, color, doubleSided — for models with holes or inconsistent faces. Vertices (v) and faces (f, including n-gons and negative indices) are read; the rest is ignored. Keep models under a few thousand faces: each face is a DOM element.
| Member | Description |
|---|---|
translate(x, y, z) |
Move the figure by the given offsets |
setPosition(x, y, z) |
Move the figure center to the given point |
rotate(ax, ay, az) |
Rotate around the figure center by X, Y, Z angles |
setTexture(url) |
Put a texture on every face |
setColor(color) |
Change the color of every face |
setStroke(color, width = 1) |
Outline every face, null removes the outline |
setShading(enabled) |
false — ignore lighting (flat neon look) |
setDoubleSided(enabled) |
true — draw faces turned away from the camera too |
position |
Current center of the figure (read only) |
Methods that change the figure return it, so they can be chained: engine.cube(10).setStroke('#0ff').setShading(false).
Faces are shaded by a directional light (enabled by default), and faces turned away from the camera are skipped. Lighting does not change textured faces.
engine.setLight({ direction: { x: -1, y: 1, z: -0.5 }, ambient: 0.3, specular: 0.5 })
engine.setFog({ color: '#cddff0', near: 100, far: 300 })
engine.setGlow({ blur: 3, strength: 2 })| Function | Options |
|---|---|
setLight(options) |
direction — direction to the light, color, intensity (0.75), ambient (0.3), specular — highlight strength (0.15), shininess (24). null turns lighting off |
setFog(options) |
color, near, far — the color fades linearly into color between these distances. Give the page background the same color. null turns fog off |
setGlow(options) |
Neon glow of the whole scene via an SVG filter: blur (4), strength (2). null turns it off. Looks best with setStroke |
engine.player contains the camera position and view direction. You can set these properties too:
engine.player.position = { x: 100, y: 150, z: 100 }
engine.player.rotation.ay = 90| Property | Description | Example |
|---|---|---|
| position | Camera position | { x: 23, y: 46, z: -100 } |
| rotation | Camera angles: ay — turn left/right, az — look up/down (±70°), ax — roll |
{ ax: 0, ay: 10, az: -10 } |
engine.player.lookAt({ x, y, z }) turns the camera towards a point, and engine.player.isControlled is true while the user controls the camera.
| Function | Description |
|---|---|
add(...objects) |
Add figures to the scene |
remove(...objects) |
Remove figures from the scene |
update(callback) |
Call callback every frame (pass null to clear) |
setBackground(urlImage) |
Panorama background that scrolls when the camera turns |
addBackgroundElement(urlImage, x, y) |
Extra background layer (clouds, mountains) with faster parallax |
start(fps = 120) / stop() |
Resume / pause the render loop |
destroy() |
Stop rendering, remove listeners and the SVG element |
saveScreen(filename, background) |
Download the current frame as a vector .svg (opens in Figma, Illustrator, Inkscape) |
toSVG(background) |
The current frame as an SVG string; background defaults to the fog color |
objects |
Figures currently on the scene |
Example of an animation:
const cube = engine.cube(10, 0, 0, 50, 'red')
engine.add(cube)
engine.update(() => cube.rotate(1, 1, 0))Requires Node.js 22.12+ and pnpm (corepack enable picks up the version from package.json).
pnpm install
pnpm dev # demo page (index.html) with hot reload
pnpm build # build dist/3dengine.dist.js
pnpm build:demo # build the demo page into demo-dist/ (deployed to GitHub Pages from master)
pnpm test # unit tests
pnpm lint # oxlint
pnpm format # oxfmt
pnpm check # typecheck + lint + format check + tests




