# Spell Book

> A thick stylized spell book (grimoire) lying on the ground: leather covers with gold corner guards, a ribbed rounded spine, a strap-and-lock clasp and a glowing rune sigil on the cover. Toggle it open to show curved page stacks with lines of ink, a ribbon bookmark and the rune hovering above the pages. Real-world tome size (about 0.28 m tall) — great as a spell pickup, wizard desk prop or quest item.

![Spell Book preview](https://codels.download/m/spell-book.png)

Multi-view sheet (3/4, front, side, top): https://codels.download/m/spell-book.sheet.png

## Facts

- Category: Items & Weapons › Consumables
- Size (W × H × D): 0.26 × 0.08 × 0.28 m — units are meters, +Y up, front faces +Z
- Origin: center of the base (y = 0)
- Triangles: 736 · Meshes: 33 · Materials: 7
- License: CC0-1.0 (Codels built-in)
- Tags: book, rune, spell-book, cartoon, low-poly, stylized, fantasy, halloween, medieval, adventure, roguelike, rpg, consumable, pickup, prop, leather, paper, emissive, game-ready, mobile-friendly, parametric, color-orange, color-red, size-tiny, poly-low
- Page: https://codels.download/models/spell-book

## Files

| Format | URL |
|---|---|
| three.js ES module (`createSpellBook()`) | https://codels.download/m/spell-book.js |
| React Three Fiber component (`<SpellBook />`) | https://codels.download/m/spell-book.jsx |
| Instancing module (`createSpellBookInstances(count, place)`, one draw call per material) | https://codels.download/m/spell-book.instanced.js |
| Standalone HTML demo | https://codels.download/m/spell-book.html |
| glTF binary | https://codels.download/m/spell-book.glb |
| glTF binary, Meshopt-compressed (2–3× smaller; GLTFLoader needs `setMeshoptDecoder(MeshoptDecoder)`) | https://codels.download/m/spell-book.glb?compress=meshopt |
| glTF JSON | https://codels.download/m/spell-book.gltf |
| OBJ (vertex colors) + MTL | https://codels.download/m/spell-book.obj · https://codels.download/m/spell-book.mtl |
| STL | https://codels.download/m/spell-book.stl |
| CodelSpec JSON | https://codels.download/m/spell-book.json |

## Parameters

Append them as query parameters to any URL above to get a customized variant (e.g. `?cover=...`).

| Param | Type | Range / options | Default | Current |
|---|---|---|---|---|
| `cover` | color | #rrggbb | #7a2a3a | #7a2a3a |
| `rune` | color | #rrggbb | #53d8ff | #53d8ff |
| `open` | boolean | true \| false | false | false |

## Parts

- backCover (box · cover)
- frontCover (box · cover)
- pageBlock (box · pages)
- pageLineSide1 (box · edge)
- pageLineFront1 (box · edge)
- pageLineSide2 (box · edge)
- pageLineFront2 (box · edge)
- pageLineSide3 (box · edge)
- pageLineFront3 (box · edge)
- spine (cylinder · cover)
- pageRound (cylinder · pages)
- spineRibBack (cylinder · dark)
- spineRibMid (cylinder · dark)
- spineRibFront (cylinder · dark)
- cornerTopFront (extrude · gold)
- cornerBottomFront (extrude · gold)
- cornerTopBack (extrude · gold)
- cornerBottomBack (extrude · gold)
- claspStrap (box · dark)
- claspDrop (box · dark)
- lockPlate (box · gold)
- keyhole (box · ink)
- frameZFront (box · gold)
- frameXRight (box · gold)
- frameZBack (box · gold)
- frameXLeft (box · gold)
- rune (group)
  - runeRing (extrude · rune)
  - runeDiamond (extrude · rune)
  - runeStroke (box · rune)
  - runeDot1 (cylinder · rune)
  - runeDot2 (cylinder · rune)
  - runeDot3 (cylinder · rune)
  - runeDot4 (cylinder · rune)

## How to use

Save the code below as `spell-book.js`, then:

```js
import { createSpellBook } from './spell-book.js';
const model = createSpellBook();
scene.add(model);
```

## Code

```js
/**
 * Spell Book — Codels 3D model library (https://codels.download/models/spell-book)
 * License: CC0-1.0 · Units: meters · Y-up · Front: +Z · Origin: center of the base (y = 0)
 * Triangles: 736 · Size (W × H × D): 0.26 × 0.08 × 0.28 m
 *
 * Usage:
 *   import { createSpellBook } from './spell-book.js';
 *   
 *   const model = createSpellBook();
 *   model.position.set(0, 0, 0);
 *   scene.add(model);
 */
import * as THREE from 'three';

const deg = THREE.MathUtils.degToRad;

// Extrude a 2D outline along Z (centered). bevel = [thickness, size, segments].
function extrude(outline, depth, holes = [], bevel = null) {
  const shape = new THREE.Shape(outline.map(([x, y]) => new THREE.Vector2(x, y)));
  for (const hole of holes) shape.holes.push(new THREE.Path(hole.map(([x, y]) => new THREE.Vector2(x, y))));
  const options = bevel
    ? { depth, bevelEnabled: true, bevelThickness: bevel[0], bevelSize: bevel[1], bevelSegments: bevel[2] }
    : { depth, bevelEnabled: false };
  return new THREE.ExtrudeGeometry(shape, options).translate(0, 0, -depth / 2);
}

export function createSpellBook() {
  const root = new THREE.Group();
  root.name = 'SpellBook';

  // Materials
  const coverMat = new THREE.MeshStandardMaterial({ name: 'cover', color: 0x7a2a3a, roughness: 0.8, flatShading: true });
  const darkMat = new THREE.MeshStandardMaterial({ name: 'dark', color: 0x491923, roughness: 0.85, flatShading: true });
  const pagesMat = new THREE.MeshStandardMaterial({ name: 'pages', color: 0xefe1bd, roughness: 0.95, flatShading: true });
  const edgeMat = new THREE.MeshStandardMaterial({ name: 'edge', color: 0xd8c59a, roughness: 0.95, flatShading: true });
  const inkMat = new THREE.MeshStandardMaterial({ name: 'ink', color: 0x4a3a30, roughness: 0.9, flatShading: true });
  const goldMat = new THREE.MeshStandardMaterial({ name: 'gold', color: 0xd9ad4c, roughness: 0.35, metalness: 0.75, flatShading: true });
  const runeMat = new THREE.MeshStandardMaterial({ name: 'rune', color: 0x8fe6ff, roughness: 0.4, emissive: 0x53d8ff, emissiveIntensity: 1.1, flatShading: true });

  // Shared geometries
  const boxGeo = new THREE.BoxGeometry(0.205, 0.008, 0.275);
  const boxGeo2 = new THREE.BoxGeometry(0.002, 0.0015, 0.255);
  const boxGeo3 = new THREE.BoxGeometry(0.181, 0.0015, 0.002);
  const cylinderGeo = new THREE.CylinderGeometry(0.0415, 0.0415, 0.014, 10, 1, false, Math.PI, Math.PI);
  const extrudeGeo = extrude([[0, 0], [-0.042, 0], [0, 0.042]], 0.011);
  const extrudeGeo2 = extrude([[0, 0], [-0.042, 0], [0, -0.042]], 0.011);
  const boxGeo4 = new THREE.BoxGeometry(0.155, 0.002, 0.006);
  const boxGeo5 = new THREE.BoxGeometry(0.006, 0.002, 0.225);
  const cylinderGeo2 = new THREE.CylinderGeometry(0.0045, 0.0045, 0.002, 6);

  // Parts
  const backCover = new THREE.Mesh(boxGeo, coverMat);
  backCover.name = 'backCover';
  backCover.position.set(0, 0.004, 0);
  root.add(backCover);
  const frontCover = new THREE.Mesh(boxGeo, coverMat);
  frontCover.name = 'frontCover';
  frontCover.position.set(0, 0.071, 0);
  root.add(frontCover);
  const pageBlock = new THREE.Mesh(new THREE.BoxGeometry(0.191, 0.059, 0.259), pagesMat);
  pageBlock.name = 'pageBlock';
  pageBlock.position.set(0.003, 0.0375, 0);
  root.add(pageBlock);
  const pageLineSide1 = new THREE.Mesh(boxGeo2, edgeMat);
  pageLineSide1.name = 'pageLineSide1';
  pageLineSide1.position.set(0.099, 0.0228, 0);
  root.add(pageLineSide1);
  const pageLineFront1 = new THREE.Mesh(boxGeo3, edgeMat);
  pageLineFront1.name = 'pageLineFront1';
  pageLineFront1.position.set(0.004, 0.0228, 0.13);
  root.add(pageLineFront1);
  const pageLineSide2 = new THREE.Mesh(boxGeo2, edgeMat);
  pageLineSide2.name = 'pageLineSide2';
  pageLineSide2.position.set(0.099, 0.0375, 0);
  root.add(pageLineSide2);
  const pageLineFront2 = new THREE.Mesh(boxGeo3, edgeMat);
  pageLineFront2.name = 'pageLineFront2';
  pageLineFront2.position.set(0.004, 0.0375, 0.13);
  root.add(pageLineFront2);
  const pageLineSide3 = new THREE.Mesh(boxGeo2, edgeMat);
  pageLineSide3.name = 'pageLineSide3';
  pageLineSide3.position.set(0.099, 0.0523, 0);
  root.add(pageLineSide3);
  const pageLineFront3 = new THREE.Mesh(boxGeo3, edgeMat);
  pageLineFront3.name = 'pageLineFront3';
  pageLineFront3.position.set(0.004, 0.0523, 0.13);
  root.add(pageLineFront3);
  const spine = new THREE.Mesh(new THREE.CylinderGeometry(0.0375, 0.0375, 0.275, 10, 1, false, Math.PI, Math.PI), coverMat);
  spine.name = 'spine';
  spine.position.set(-0.1025, 0.0375, 0);
  spine.rotation.set(deg(90), 0, 0);
  root.add(spine);
  const pageRound = new THREE.Mesh(new THREE.CylinderGeometry(0.0295, 0.0295, 0.259, 10, 1, false, Math.PI, Math.PI), pagesMat);
  pageRound.name = 'pageRound';
  pageRound.position.set(-0.1015, 0.0375, 0);
  pageRound.rotation.set(deg(90), 0, 0);
  root.add(pageRound);
  const spineRibBack = new THREE.Mesh(cylinderGeo, darkMat);
  spineRibBack.name = 'spineRibBack';
  spineRibBack.position.set(-0.1025, 0.0375, -0.085);
  spineRibBack.rotation.set(deg(90), 0, 0);
  root.add(spineRibBack);
  const spineRibMid = new THREE.Mesh(cylinderGeo, darkMat);
  spineRibMid.name = 'spineRibMid';
  spineRibMid.position.set(-0.1025, 0.0375, 0);
  spineRibMid.rotation.set(deg(90), 0, 0);
  root.add(spineRibMid);
  const spineRibFront = new THREE.Mesh(cylinderGeo, darkMat);
  spineRibFront.name = 'spineRibFront';
  spineRibFront.position.set(-0.1025, 0.0375, 0.085);
  spineRibFront.rotation.set(deg(90), 0, 0);
  root.add(spineRibFront);
  const cornerTopFront = new THREE.Mesh(extrudeGeo, goldMat);
  cornerTopFront.name = 'cornerTopFront';
  cornerTopFront.position.set(0.1045, 0.071, 0.1395);
  cornerTopFront.rotation.set(deg(-90), 0, 0);
  root.add(cornerTopFront);
  const cornerBottomFront = new THREE.Mesh(extrudeGeo, goldMat);
  cornerBottomFront.name = 'cornerBottomFront';
  cornerBottomFront.position.set(0.1045, 0.004, 0.1395);
  cornerBottomFront.rotation.set(deg(-90), 0, 0);
  root.add(cornerBottomFront);
  const cornerTopBack = new THREE.Mesh(extrudeGeo2, goldMat);
  cornerTopBack.name = 'cornerTopBack';
  cornerTopBack.position.set(0.1045, 0.071, -0.1395);
  cornerTopBack.rotation.set(deg(-90), 0, 0);
  root.add(cornerTopBack);
  const cornerBottomBack = new THREE.Mesh(extrudeGeo2, goldMat);
  cornerBottomBack.name = 'cornerBottomBack';
  cornerBottomBack.position.set(0.1045, 0.004, -0.1395);
  cornerBottomBack.rotation.set(deg(-90), 0, 0);
  root.add(cornerBottomBack);
  const claspStrap = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.004, 0.036), darkMat);
  claspStrap.name = 'claspStrap';
  claspStrap.position.set(0.0805, 0.077, 0);
  root.add(claspStrap);
  const claspDrop = new THREE.Mesh(new THREE.BoxGeometry(0.006, 0.0465, 0.036), darkMat);
  claspDrop.name = 'claspDrop';
  claspDrop.position.set(0.1055, 0.0548, 0);
  root.add(claspDrop);
  const lockPlate = new THREE.Mesh(new THREE.BoxGeometry(0.008, 0.03, 0.05), goldMat);
  lockPlate.name = 'lockPlate';
  lockPlate.position.set(0.1065, 0.0315, 0);
  root.add(lockPlate);
  const keyhole = new THREE.Mesh(new THREE.BoxGeometry(0.004, 0.012, 0.006), inkMat);
  keyhole.name = 'keyhole';
  keyhole.position.set(0.1105, 0.0307, 0);
  root.add(keyhole);
  const frameZFront = new THREE.Mesh(boxGeo4, goldMat);
  frameZFront.name = 'frameZFront';
  frameZFront.position.set(0.006, 0.076, 0.1155);
  root.add(frameZFront);
  const frameXRight = new THREE.Mesh(boxGeo5, goldMat);
  frameXRight.name = 'frameXRight';
  frameXRight.position.set(0.0805, 0.076, 0);
  root.add(frameXRight);
  const frameZBack = new THREE.Mesh(boxGeo4, goldMat);
  frameZBack.name = 'frameZBack';
  frameZBack.position.set(0.006, 0.076, -0.1155);
  root.add(frameZBack);
  const frameXLeft = new THREE.Mesh(boxGeo5, goldMat);
  frameXLeft.name = 'frameXLeft';
  frameXLeft.position.set(-0.0685, 0.076, 0);
  root.add(frameXLeft);
  const rune = new THREE.Group();
  rune.name = 'rune';
  rune.position.set(0.006, 0.076, 0);
  rune.rotation.set(deg(-90), 0, 0);
  root.add(rune);
  const runeRing = new THREE.Mesh(extrude([[0.046, 0], [0.0437, 0.0142], [0.0372, 0.027], [0.027, 0.0372], [0.0142, 0.0437], [0, 0.046], [-0.0142, 0.0437], [-0.027, 0.0372], [-0.0372, 0.027], [-0.0437, 0.0142], [-0.046, 0], [-0.0437, -0.0142], [-0.0372, -0.027], [-0.027, -0.0372], [-0.0142, -0.0437], [0, -0.046], [0.0142, -0.0437], [0.027, -0.0372], [0.0372, -0.027], [0.0437, -0.0142]], 0.002, [[[0.0371, -0.0121], [0.0316, -0.0229], [0.0229, -0.0316], [0.0121, -0.0371], [0, -0.039], [-0.0121, -0.0371], [-0.0229, -0.0316], [-0.0316, -0.0229], [-0.0371, -0.0121], [-0.039, 0], [-0.0371, 0.0121], [-0.0316, 0.0229], [-0.0229, 0.0316], [-0.0121, 0.0371], [0, 0.039], [0.0121, 0.0371], [0.0229, 0.0316], [0.0316, 0.0229], [0.0371, 0.0121], [0.039, 0]]]), runeMat);
  runeRing.name = 'runeRing';
  rune.add(runeRing);
  const runeDiamond = new THREE.Mesh(extrude([[0, 0.0324], [0.024, 0], [0, -0.0324], [-0.024, 0]], 0.002, [[[-0.017, 0], [0, -0.023], [0.017, 0], [0, 0.023]]]), runeMat);
  runeDiamond.name = 'runeDiamond';
  rune.add(runeDiamond);
  const runeStroke = new THREE.Mesh(new THREE.BoxGeometry(0.004, 0.05, 0.002), runeMat);
  runeStroke.name = 'runeStroke';
  rune.add(runeStroke);
  const runeDot1 = new THREE.Mesh(cylinderGeo2, runeMat);
  runeDot1.name = 'runeDot1';
  runeDot1.position.set(0.0226, 0.0226, 0);
  runeDot1.rotation.set(deg(90), 0, 0);
  rune.add(runeDot1);
  const runeDot2 = new THREE.Mesh(cylinderGeo2, runeMat);
  runeDot2.name = 'runeDot2';
  runeDot2.position.set(-0.0226, 0.0226, 0);
  runeDot2.rotation.set(deg(90), 0, 0);
  rune.add(runeDot2);
  const runeDot3 = new THREE.Mesh(cylinderGeo2, runeMat);
  runeDot3.name = 'runeDot3';
  runeDot3.position.set(-0.0226, -0.0226, 0);
  runeDot3.rotation.set(deg(90), 0, 0);
  rune.add(runeDot3);
  const runeDot4 = new THREE.Mesh(cylinderGeo2, runeMat);
  runeDot4.name = 'runeDot4';
  runeDot4.position.set(0.0226, -0.0226, 0);
  runeDot4.rotation.set(deg(90), 0, 0);
  rune.add(runeDot4);

  root.traverse((o) => {
    if (o.isMesh) {
      o.castShadow = true;
      o.receiveShadow = true;
    }
  });
  return root;
}

export default createSpellBook;
```

---
Codels — 3D models as code for three.js. Search more: https://codels.download/api/v1/models?q=<query> · Guide: https://codels.download/llms.txt
