mana-engine
v0.1.1
Published
A small, fast 3D engine for the web: one scene graph, two backends (WebGPU + WebGL2), Z-up, shader hooks, and an optional React renderer.
Maintainers
Readme
Mana Engine
A small, high-performance 3D engine for the web that renders the same scene on WebGPU or WebGL2, with an optional React renderer. If you've used Three.js, React Three Fiber, or Drei, you already know the API — Mana Engine is a deliberate subset of all three, trimmed down to what a real-time game actually uses, and written in TypeScript with a right-handed Z-up world.
It was purpose-built for Mana Blade, a low-poly multiplayer online RPG, so everything in it is tested in a shipped game.
import {
BoxGeometry,
DirectionalLight,
Mesh,
MeshLambertMaterial,
PerspectiveCamera,
Scene,
WebGPURenderer,
} from 'mana-engine'
const scene = new Scene()
const camera = new PerspectiveCamera(50, width / height, 0.1, 100)
camera.position.set(0, -6, 4) // Z-up: pull back along -Y, up along +Z
camera.lookAt(0, 0, 0)
const light = new DirectionalLight(0xffffff, 1)
light.position.set(4, -5, 6)
light.castShadow = true
scene.add(light)
scene.add(new Mesh(new BoxGeometry(1, 1, 1), new MeshLambertMaterial({ color: 0x88ccff })))
const renderer = new WebGPURenderer({ canvas, antialias: true })
await renderer.init()
renderer.shadowMap.enabled = true
renderer.setSize(width, height)
renderer.render(scene, camera)Or the same thing in React:
import { useRef } from 'react'
import { Canvas, useFrame } from 'mana-engine/react'
function Spinner() {
const ref = useRef()
useFrame((_, dt) => (ref.current.rotation.z += dt))
return (
<mesh ref={ref} castShadow>
<boxGeometry args={[1, 1, 1]} />
<meshLambertMaterial color="skyblue" />
</mesh>
)
}
export default () => (
<Canvas shadows camera={{ position: [0, -6, 4] }}>
<ambientLight intensity={0.5} />
<directionalLight position={[4, -5, 6]} castShadow />
<Spinner />
</Canvas>
)Features
- One scene graph, two backends. WebGPU and WebGL2 render identical output from the same
scene.
<Canvas>picks WebGPU when available and falls back to WebGL2 — including at runtime if the GPU device is ever lost mid-session. - Custom shaders in plain GLSL and WGSL. Materials accept shader hooks: hand-written snippets injected at fixed points in the built-in shaders (vertex displacement, albedo, lighting, post-lighting tints, billboard placement), with typed uniforms, attributes, and varyings. No shader graph, no compiler — what you write is what runs.
- Characters and effects. GPU skinning,
InstancedMesh, and billboard sprites with per-instance attributes for stateless GPU particle systems. - Directional shadows. Soft PCF shadows that automatically get cheaper in the distance, an optional per-vertex mode for dense foliage, and a freeze switch so a static zone-wide shadow map renders once and gets reused.
- Baked-lighting friendly. A pluggable indirect-lighting hook and 3D textures make it easy to feed in baked GI or irradiance volumes; ambient occlusion and wrap (half-lambert) lighting are built into the lambert material.
- Smooth loading. Shaders compile in parallel on both backends (async pipelines on WebGPU,
KHR_parallel_shader_compileon WebGL2) and never stall a frame;renderer.isCompiling()lets you hold a loading screen until everything is warm. - Compact assets.
GLTFLoaderdecodes Draco-compressed GLBs through pure-TypeScript minidraco — no WASM files to host — and gputex compresses textures to BC/ASTC/ETC2 on the GPU at load time. - A React renderer you already know.
Canvas, JSX intrinsics for every object,useFramewith priorities,useThree, Suspense-baseduseLoader/useGLTF, pointer events with bubbling, anHtmloverlay, and familiar components likeOrbitControls(touch included),Stats,useProgress, andPerformanceMonitor. - Fast raycasting. A built-in triangle BVH (
MeshBVH) acceleratesRaycasteragainst large meshes like terrain. - In-shader color grading. Photoshop-style levels (
blackPoint/whitePoint) applied at the end of every fragment shader — free of any post-processing pass.
Installation
npm install mana-engine # or: bun add mana-engineReact (and react-dom for the Html overlay) are optional peer dependencies — install them only
if you use mana-engine/react.
Entry points
| Import | What you get |
| ------------------------------ | ------------------------------------------------------------------ |
| mana-engine | The engine: math, scene graph, materials, loaders, both renderers. |
| mana-engine/react | The React renderer: Canvas, hooks, events, components. |
| mana-engine/shader-templates | Low-level shader template builders, for external shader tooling. |
Writing a custom shader
A material's hooks object carries paired snippets — GLSL for WebGL2, WGSL for WebGPU. Provide
one language to support one backend, both to support both:
import { MeshBasicMaterial, type ShaderHooks } from 'mana-engine'
const hooks: ShaderHooks = {
key: 'stripes',
uniforms: { uSpeed: { type: 'float', value: 1 } },
colorGLSL: `
float wave = sin(vUv.x * 10.0 + time * uSpeed);
mana_color = vec4(mix(vec3(0.0), vec3(1.0, 0.5, 0.0), wave), opacity);`,
colorWGSL: `
let wave = sin(vUv.x * 10.0 + time * mana_u.uSpeed);
mana_color = vec4f(mix(vec3f(0.0), vec3f(1.0, 0.5, 0.0), wave), mana_obj.opacity);`,
}
const material = new MeshBasicMaterial()
material.hooks = hooks
hooks.uniforms!.uSpeed!.value = 2 // uniforms are live — mutate .value to animateSnippets see a stable set of names that works identically in both languages (mana_position,
mana_color, time, cameraPosition, …), and everything the engine injects is prefixed mana_
so it never collides with your code. Hooks with the same key share compiled programs. The full
contract is documented in
src/renderer/hooks-contract.ts.
Loading assets
import { useGLTF, useGputex } from 'mana-engine/react'
const { scene, animations } = useGLTF('/models/hero.glb') // Draco-decoded via minidraco
const albedo = useGputex('/textures/atlas.png', { hint: 'color', mipmaps: true }) // GPU-compressed via gputexThe imperative equivalents (loadGLTF, GLTFLoader, GputexLoader) live in the core entry.
What's in the box
- Math —
Vector2/3/4,Matrix3/4,Quaternion,Euler,Color,Box3,Sphere,Plane,Ray,Frustum,Triangle,MathUtils, and the interpolant family. - Scene graph & objects —
Object3D,Group,Mesh,SkinnedMesh,Bone,Skeleton,Sprite,InstancedMesh, lines,Scene,Fog,Raycaster,Clock,Layers. - Cameras & lights —
PerspectiveCamera,OrthographicCamera,AmbientLight,DirectionalLightwith shadows. - Materials —
MeshBasicMaterial(unlit),MeshLambertMaterial,SpriteMaterial,LineBasicMaterial— all hookable, with additive / subtractive / multiply / overlay blending. - Geometries — box, sphere, capsule, cone, cylinder, plane, circle, ring, torus, icosahedron,
octahedron, polyhedron, shape (with the full curve family and earcut triangulation). Standard
names are Y-up like Three.js;
*ZUpvariants (ConeGeometryZUp, …) stand upright in a Z-up world with no rotation. - Textures —
Texture,DataTexture,Data3DTexture,CompressedTexture. - Animation —
AnimationMixer, clips, actions, and typed keyframe tracks, fed by the GLTF loader. - Extras —
SkeletonUtilsfor cloning rigs, geometry merge/weld utilities (mergeGeometries,mergeVertices,toCreasedNormals), and the BVH toolkit.
Conventions
- Z-up, right-handed, no surprises.
Object3D.DEFAULT_UPis(0, 0, 1)and cameras, controls, shadows, and loaders all agree — GLB data authored Z-up passes through untouched. - Performance is a feature. Hot paths avoid allocation, uniforms only upload when they change,
and draw calls are sorted for state coherence.
renderer.inforeports draw stats, and opt-in GPU timestamp queries measure real GPU time. - Both backends or nothing. Every feature ships on WebGPU and WebGL2 together, with matched output — including workarounds for real-world mobile driver bugs.
What it doesn't do
Mana Engine is deliberately small. There is no postprocessing stack, no point/spot lights, no environment maps or PMREM, no morph targets, and no WebXR. If a stylized real-time game doesn't need it, it's probably not here — features land when a real use case demands them, and always on both backends at once.
Development
bun install
bun run typecheck
bun testtest-pages/ contains standalone scenes exercising the engine end to end, and scripts/ has
headless render-regression drivers. The package's exports point at the TypeScript source
directly; publishing builds a bundled dist/ (bun run build).
Mana Engine currently lives in Mana Blade's private repository. I will sync this repo and publish NPM versions for external use every now and then, but please keep in mind that my first priority is the game itself. The public version might not always be fully up-to-date, and I will likely reject proposed changes that are not relevant for Mana Blade.
License
MIT. The math, scene-graph, geometry, and animation classes are re-authored to match
the Three.js r185 API shape; the React layer is modeled on React Three Fiber and Drei; polygon
triangulation is a verbatim port of Mapbox's earcut (ISC). Full attribution in
LICENSE-THIRD-PARTY.md.
