# Ninja

> A stealthy, chibi-proportioned low-poly ninja in a wrapped shinobi outfit with a full hood and eye slit, determined eyes, a headband with a steel plate and flowing tails, a wide sash, shin and forearm wraps, and a katana strapped diagonally across the back with the hilt over the left shoulder. Recolor outfit and accents for rival clans. Head, arms and legs are pivoted groups for sneak, run and attack animations in action, platformer and Japanese-themed games.

![Ninja preview](https://codels.download/m/ninja.png)

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

## Facts

- Category: Characters › Humanoids
- Size (W × H × D): 0.85 × 1.55 × 0.77 m — units are meters, +Y up, front faces +Z
- Origin: center of the base (y = 0)
- Triangles: 676 · Meshes: 55 · Materials: 8
- License: CC0-1.0 (Codels built-in)
- Tags: katana, ninja, stealth, sword, warrior, blocky, cartoon, low-poly, japanese, adventure, platformer, rpg, character, enemy, npc, game-ready, mobile-friendly, parametric, color-blue, size-medium, poly-low
- Page: https://codels.download/models/ninja

## Files

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

## Parameters

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

| Param | Type | Range / options | Default | Current |
|---|---|---|---|---|
| `outfit` | color | #rrggbb | #2b2e3d | #2b2e3d |
| `accent` | color | #rrggbb | #d23a3f | #d23a3f |
| `katana` | boolean | true \| false | true | true |

## Parts

- legL (group)
  - pantsL (box · outfit)
  - shinWrapL (box · wrap)
  - wrapLineL (box · outfit)
  - wrapLineL (box · outfit)
  - tabiL (box · outfit)
  - bigToeL (box · outfit)
- legR (group)
  - pantsR (box · outfit)
  - shinWrapR (box · wrap)
  - wrapLineR (box · outfit)
  - wrapLineR (box · outfit)
  - tabiR (box · outfit)
  - bigToeR (box · outfit)
- body (group)
  - torso (box · outfit)
  - lapelL (box · wrap)
  - lapelR (box · wrap)
  - sash (box · accent)
  - sashKnot (box · accent)
  - sashTail1 (box · accent)
  - sashTail2 (box · accent)
  - strapFront (box · sheath)
  - strapShoulder (box · sheath)
  - katana (group)
    - saya (box · sheath)
    - sayaBand (box · accent)
    - sayaTip (box · steel)
    - tsuba (cylinder · steel)
    - tsuka (box · wrap)
    - tsukaWrap (box · sheath)
    - tsukaWrap (box · sheath)
    - tsukaWrap (box · sheath)
    - tsukaWrap (box · sheath)
    - kashira (box · steel)
- armL (group)
  - sleeveL (box · outfit)
  - forearmWrapL (box · wrap)
  - armWrapLineL (box · outfit)
  - gloveL (box · outfit)

## How to use

Save the code below as `ninja.js`, then:

```js
import { createNinja } from './ninja.js';
const model = createNinja();
scene.add(model);
```

## Code

```js
/**
 * Ninja — Codels 3D model library (https://codels.download/models/ninja)
 * License: CC0-1.0 · Units: meters · Y-up · Front: +Z · Origin: center of the base (y = 0)
 * Triangles: 676 · Size (W × H × D): 0.85 × 1.55 × 0.77 m
 *
 * Usage:
 *   import { createNinja } from './ninja.js';
 *   
 *   const model = createNinja();
 *   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 createNinja() {
  const root = new THREE.Group();
  root.name = 'Ninja';

  // Materials
  const outfitMat = new THREE.MeshStandardMaterial({ name: 'outfit', color: 0x2b2e3d, roughness: 0.85, flatShading: true });
  const wrapMat = new THREE.MeshStandardMaterial({ name: 'wrap', color: 0x727581, roughness: 0.9, flatShading: true });
  const accentMat = new THREE.MeshStandardMaterial({ name: 'accent', color: 0xd23a3f, roughness: 0.8, flatShading: true });
  const skinMat = new THREE.MeshStandardMaterial({ name: 'skin', color: 0xf0c29b, roughness: 0.8, flatShading: true });
  const eyeMat = new THREE.MeshStandardMaterial({ name: 'eye', color: 0x18151a, roughness: 0.3, flatShading: true });
  const shineMat = new THREE.MeshStandardMaterial({ name: 'shine', color: 0xffffff, roughness: 0.3, flatShading: true });
  const steelMat = new THREE.MeshStandardMaterial({ name: 'steel', color: 0xc6ccd4, roughness: 0.25, metalness: 0.8, flatShading: true });
  const sheathMat = new THREE.MeshStandardMaterial({ name: 'sheath', color: 0x141216, roughness: 0.35, flatShading: true });

  // Shared geometries
  const boxGeo = new THREE.BoxGeometry(0.2, 0.3, 0.22);
  const boxGeo2 = new THREE.BoxGeometry(0.18, 0.2, 0.19);
  const boxGeo3 = new THREE.BoxGeometry(0.184, 0.02, 0.194);
  const boxGeo4 = new THREE.BoxGeometry(0.19, 0.08, 0.26);
  const boxGeo5 = new THREE.BoxGeometry(0.06, 0.06, 0.05);
  const boxGeo6 = new THREE.BoxGeometry(0.08, 0.08, 0.05);
  const boxGeo7 = new THREE.BoxGeometry(0.054, 0.016, 0.049);
  const boxGeo8 = new THREE.BoxGeometry(0.16, 0.22, 0.18);
  const boxGeo9 = new THREE.BoxGeometry(0.14, 0.18, 0.16);
  const boxGeo10 = new THREE.BoxGeometry(0.144, 0.018, 0.164);
  const boxGeo11 = new THREE.BoxGeometry(0.13, 0.11, 0.15);
  const boxGeo12 = new THREE.BoxGeometry(0.06, 0.07, 0.01);
  const boxGeo13 = new THREE.BoxGeometry(0.0228, 0.0228, 0.006);
  const boxGeo14 = new THREE.BoxGeometry(0.11, 0.03, 0.02);

  // Parts
  const legL = new THREE.Group();
  legL.name = 'legL';
  legL.position.set(0.105, 0.58, 0);
  root.add(legL);
  const pantsL = new THREE.Mesh(boxGeo, outfitMat);
  pantsL.name = 'pantsL';
  pantsL.position.set(0, -0.15, 0);
  legL.add(pantsL);
  const shinWrapL = new THREE.Mesh(boxGeo2, wrapMat);
  shinWrapL.name = 'shinWrapL';
  shinWrapL.position.set(0, -0.4, 0);
  legL.add(shinWrapL);
  const wrapLineL = new THREE.Mesh(boxGeo3, outfitMat);
  wrapLineL.name = 'wrapLineL';
  wrapLineL.position.set(0, -0.34, 0);
  wrapLineL.rotation.set(0, 0, deg(-6));
  legL.add(wrapLineL);
  const wrapLineL2 = new THREE.Mesh(boxGeo3, outfitMat);
  wrapLineL2.name = 'wrapLineL';
  wrapLineL2.position.set(0, -0.41, 0);
  wrapLineL2.rotation.set(0, 0, deg(6));
  legL.add(wrapLineL2);
  const tabiL = new THREE.Mesh(boxGeo4, outfitMat);
  tabiL.name = 'tabiL';
  tabiL.position.set(0, -0.54, 0.03);
  legL.add(tabiL);
  const bigToeL = new THREE.Mesh(boxGeo5, outfitMat);
  bigToeL.name = 'bigToeL';
  bigToeL.position.set(0.055, -0.55, 0.175);
  legL.add(bigToeL);
  const legR = new THREE.Group();
  legR.name = 'legR';
  legR.position.set(-0.105, 0.58, 0);
  root.add(legR);
  const pantsR = new THREE.Mesh(boxGeo, outfitMat);
  pantsR.name = 'pantsR';
  pantsR.position.set(0, -0.15, 0);
  legR.add(pantsR);
  const shinWrapR = new THREE.Mesh(boxGeo2, wrapMat);
  shinWrapR.name = 'shinWrapR';
  shinWrapR.position.set(0, -0.4, 0);
  legR.add(shinWrapR);
  const wrapLineR = new THREE.Mesh(boxGeo3, outfitMat);
  wrapLineR.name = 'wrapLineR';
  wrapLineR.position.set(0, -0.34, 0);
  wrapLineR.rotation.set(0, 0, deg(6));
  legR.add(wrapLineR);
  const wrapLineR2 = new THREE.Mesh(boxGeo3, outfitMat);
  wrapLineR2.name = 'wrapLineR';
  wrapLineR2.position.set(0, -0.41, 0);
  wrapLineR2.rotation.set(0, 0, deg(-6));
  legR.add(wrapLineR2);
  const tabiR = new THREE.Mesh(boxGeo4, outfitMat);
  tabiR.name = 'tabiR';
  tabiR.position.set(0, -0.54, 0.03);
  legR.add(tabiR);
  const bigToeR = new THREE.Mesh(boxGeo5, outfitMat);
  bigToeR.name = 'bigToeR';
  bigToeR.position.set(-0.055, -0.55, 0.175);
  legR.add(bigToeR);
  const body = new THREE.Group();
  body.name = 'body';
  root.add(body);
  const torso = new THREE.Mesh(new THREE.BoxGeometry(0.44, 0.56, 0.26), outfitMat);
  torso.name = 'torso';
  torso.position.set(0, 0.8, 0);
  body.add(torso);
  const lapelL = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.4428, 0.01), wrapMat);
  lapelL.name = 'lapelL';
  lapelL.position.set(0.025, 0.88, 0.133);
  lapelL.rotation.set(deg(-90), deg(-64.5923), deg(90));
  body.add(lapelL);
  const lapelR = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.236, 0.01), wrapMat);
  lapelR.name = 'lapelR';
  lapelR.position.set(-0.05, 0.985, 0.131);
  lapelR.rotation.set(deg(-90), deg(53.6156), deg(-90));
  body.add(lapelR);
  const sash = new THREE.Mesh(new THREE.BoxGeometry(0.452, 0.11, 0.272), accentMat);
  sash.name = 'sash';
  sash.position.set(0, 0.61, 0);
  body.add(sash);
  const sashKnot = new THREE.Mesh(boxGeo6, accentMat);
  sashKnot.name = 'sashKnot';
  sashKnot.position.set(0.15, 0.61, 0.15);
  body.add(sashKnot);
  const sashTail1 = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.16, 0.02), accentMat);
  sashTail1.name = 'sashTail1';
  sashTail1.position.set(0.17, 0.5, 0.16);
  sashTail1.rotation.set(0, 0, deg(12));
  body.add(sashTail1);
  const sashTail2 = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.12, 0.02), accentMat);
  sashTail2.name = 'sashTail2';
  sashTail2.position.set(0.12, 0.52, 0.165);
  sashTail2.rotation.set(0, 0, deg(-8));
  body.add(sashTail2);
  const strapFront = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.5304, 0.01), sheathMat);
  strapFront.name = 'strapFront';
  strapFront.position.set(-0.015, 0.89, 0.137);
  strapFront.rotation.set(deg(-90), deg(-45.7639), deg(-90));
  body.add(strapFront);
  const strapShoulder = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.07, 0.28), sheathMat);
  strapShoulder.name = 'strapShoulder';
  strapShoulder.position.set(0.17, 1.1, 0);
  body.add(strapShoulder);
  const katana = new THREE.Group();
  katana.name = 'katana';
  katana.position.set(-0.03, 0.97, -0.165);
  katana.rotation.set(0, 0, deg(-44));
  body.add(katana);
  const saya = new THREE.Mesh(new THREE.BoxGeometry(0.07, 0.7, 0.045), sheathMat);
  saya.name = 'saya';
  saya.position.set(0, -0.1, 0);
  katana.add(saya);
  const sayaBand = new THREE.Mesh(new THREE.BoxGeometry(0.074, 0.03, 0.05), accentMat);
  sayaBand.name = 'sayaBand';
  sayaBand.position.set(0, 0.14, 0);
  katana.add(sayaBand);
  const sayaTip = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.04, 0.05), steelMat);
  sayaTip.name = 'sayaTip';
  sayaTip.position.set(0, -0.44, 0);
  katana.add(sayaTip);
  const tsuba = new THREE.Mesh(new THREE.CylinderGeometry(0.065, 0.065, 0.025, 8), steelMat);
  tsuba.name = 'tsuba';
  tsuba.position.set(0, 0.265, 0);
  katana.add(tsuba);
  const tsuka = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.26, 0.045), wrapMat);
  tsuka.name = 'tsuka';
  tsuka.position.set(0, 0.405, 0);
  katana.add(tsuka);
  const tsukaWrap = new THREE.Mesh(boxGeo7, sheathMat);
  tsukaWrap.name = 'tsukaWrap';
  tsukaWrap.position.set(0, 0.31, 0);
  tsukaWrap.rotation.set(0, 0, deg(-20));
  katana.add(tsukaWrap);
  const tsukaWrap2 = new THREE.Mesh(boxGeo7, sheathMat);
  tsukaWrap2.name = 'tsukaWrap';
  tsukaWrap2.position.set(0, 0.37, 0);
  tsukaWrap2.rotation.set(0, 0, deg(20));
  katana.add(tsukaWrap2);
  const tsukaWrap3 = new THREE.Mesh(boxGeo7, sheathMat);
  tsukaWrap3.name = 'tsukaWrap';
  tsukaWrap3.position.set(0, 0.43, 0);
  tsukaWrap3.rotation.set(0, 0, deg(-20));
  katana.add(tsukaWrap3);
  const tsukaWrap4 = new THREE.Mesh(boxGeo7, sheathMat);
  tsukaWrap4.name = 'tsukaWrap';
  tsukaWrap4.position.set(0, 0.49, 0);
  tsukaWrap4.rotation.set(0, 0, deg(20));
  katana.add(tsukaWrap4);
  const kashira = new THREE.Mesh(new THREE.BoxGeometry(0.058, 0.03, 0.052), steelMat);
  kashira.name = 'kashira';
  kashira.position.set(0, 0.545, 0);
  katana.add(kashira);
  const armL = new THREE.Group();
  armL.name = 'armL';
  armL.position.set(0.3, 1.04, 0);
  armL.rotation.set(0, 0, deg(7));
  root.add(armL);
  const sleeveL = new THREE.Mesh(boxGeo8, outfitMat);
  sleeveL.name = 'sleeveL';
  sleeveL.position.set(0, -0.09, 0);
  armL.add(sleeveL);
  const forearmWrapL = new THREE.Mesh(boxGeo9, wrapMat);
  forearmWrapL.name = 'forearmWrapL';
  forearmWrapL.position.set(0, -0.29, 0);
  armL.add(forearmWrapL);
  const armWrapLineL = new THREE.Mesh(boxGeo10, outfitMat);
  armWrapLineL.name = 'armWrapLineL';
  armWrapLineL.position.set(0, -0.27, 0);
  armWrapLineL.rotation.set(0, 0, deg(8));
  armL.add(armWrapLineL);
  const gloveL = new THREE.Mesh(boxGeo11, outfitMat);
  gloveL.name = 'gloveL';
  gloveL.position.set(0, -0.43, 0);
  armL.add(gloveL);
  const armR = new THREE.Group();
  armR.name = 'armR';
  armR.position.set(-0.3, 1.04, 0);
  armR.rotation.set(0, 0, deg(-7));
  root.add(armR);
  const sleeveR = new THREE.Mesh(boxGeo8, outfitMat);
  sleeveR.name = 'sleeveR';
  sleeveR.position.set(0, -0.09, 0);
  armR.add(sleeveR);
  const forearmWrapR = new THREE.Mesh(boxGeo9, wrapMat);
  forearmWrapR.name = 'forearmWrapR';
  forearmWrapR.position.set(0, -0.29, 0);
  armR.add(forearmWrapR);
  const armWrapLineR = new THREE.Mesh(boxGeo10, outfitMat);
  armWrapLineR.name = 'armWrapLineR';
  armWrapLineR.position.set(0, -0.27, 0);
  armWrapLineR.rotation.set(0, 0, deg(-8));
  armR.add(armWrapLineR);
  const gloveR = new THREE.Mesh(boxGeo11, outfitMat);
  gloveR.name = 'gloveR';
  gloveR.position.set(0, -0.43, 0);
  armR.add(gloveR);
  const head = new THREE.Group();
  head.name = 'head';
  head.position.set(0, 1.06, 0);
  root.add(head);
  const neck = new THREE.Mesh(new THREE.BoxGeometry(0.16, 0.06, 0.16), outfitMat);
  neck.name = 'neck';
  neck.position.set(0, 0.02, 0);
  head.add(neck);
  const hood = new THREE.Mesh(new THREE.BoxGeometry(0.46, 0.46, 0.46), outfitMat);
  hood.name = 'hood';
  hood.position.set(0, 0.26, 0);
  head.add(hood);
  const eyeSlit = new THREE.Mesh(new THREE.BoxGeometry(0.38, 0.13, 0.012), skinMat);
  eyeSlit.name = 'eyeSlit';
  eyeSlit.position.set(0, 0.28, 0.232);
  head.add(eyeSlit);
  const eyeL = new THREE.Mesh(boxGeo12, eyeMat);
  eyeL.name = 'eyeL';
  eyeL.position.set(0.09, 0.275, 0.239);
  head.add(eyeL);
  const eyeShineL = new THREE.Mesh(boxGeo13, shineMat);
  eyeShineL.name = 'eyeShineL';
  eyeShineL.position.set(0.0984, 0.2904, 0.245);
  head.add(eyeShineL);
  const browL = new THREE.Mesh(boxGeo14, outfitMat);
  browL.name = 'browL';
  browL.position.set(0.09, 0.335, 0.238);
  browL.rotation.set(0, 0, deg(14));
  head.add(browL);
  const eyeR = new THREE.Mesh(boxGeo12, eyeMat);
  eyeR.name = 'eyeR';
  eyeR.position.set(-0.09, 0.275, 0.239);
  head.add(eyeR);
  const eyeShineR = new THREE.Mesh(boxGeo13, shineMat);
  eyeShineR.name = 'eyeShineR';
  eyeShineR.position.set(-0.0984, 0.2904, 0.245);
  head.add(eyeShineR);
  const browR = new THREE.Mesh(boxGeo14, outfitMat);
  browR.name = 'browR';
  browR.position.set(-0.09, 0.335, 0.238);
  browR.rotation.set(0, 0, deg(-14));
  head.add(browR);
  const maskNose = new THREE.Mesh(extrude([[-0.05, 0], [0.05, 0], [0, 0.05]], 0.03), outfitMat);
  maskNose.name = 'maskNose';
  maskNose.position.set(0, 0.16, 0.24);
  head.add(maskNose);
  const headband = new THREE.Mesh(new THREE.BoxGeometry(0.472, 0.065, 0.472), accentMat);
  headband.name = 'headband';
  headband.position.set(0, 0.39, 0);
  head.add(headband);
  const headbandPlate = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.06, 0.012), steelMat);
  headbandPlate.name = 'headbandPlate';
  headbandPlate.position.set(0, 0.39, 0.242);
  head.add(headbandPlate);
  const plateMark = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.012, 0.004), outfitMat);
  plateMark.name = 'plateMark';
  plateMark.position.set(0, 0.39, 0.249);
  head.add(plateMark);
  const headbandKnot = new THREE.Mesh(boxGeo6, accentMat);
  headbandKnot.name = 'headbandKnot';
  headbandKnot.position.set(0.04, 0.39, -0.255);
  head.add(headbandKnot);
  const headbandTails = new THREE.Group();
  headbandTails.name = 'headbandTails';
  headbandTails.position.set(0.04, 0.39, -0.27);
  head.add(headbandTails);
  const tail1 = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.03, 0.26), accentMat);
  tail1.name = 'tail1';
  tail1.position.set(0.03, -0.02, -0.12);
  tail1.rotation.set(deg(-25), deg(18), 0);
  headbandTails.add(tail1);
  const tail2 = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.03, 0.22), accentMat);
  tail2.name = 'tail2';
  tail2.position.set(-0.03, -0.05, -0.1);
  tail2.rotation.set(deg(-40), deg(-14), 0);
  headbandTails.add(tail2);

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

export default createNinja;
```

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