Codels
604 tris0.88 × 1.82 × 0.84 m

Use it in code

Remix in the editor
Self-contained ES module — only depends on three. Call the factory and add the group to your scene.
/**
 * Villager — Codels 3D model library
 * License: CC0-1.0 · Units: meters · Y-up · Front: +Z · Origin: center of the base (y = 0)
 * Triangles: 604 · Size (W × H × D): 0.88 × 1.82 × 0.84 m
 *
 * Usage:
 *   import { createVillager } from './villager.js';
 *   
 *   const model = createVillager();
 *   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 createVillager() {
  const root = new THREE.Group();
  root.name = 'Villager';

  // Materials
  const skinMat = new THREE.MeshStandardMaterial({ name: 'skin', color: 0xf0c39c, roughness: 0.8, flatShading: true });
  const hairMat = new THREE.MeshStandardMaterial({ name: 'hair', color: 0x7a4a2a, roughness: 0.9, flatShading: true });
  const tunicMat = new THREE.MeshStandardMaterial({ name: 'tunic', color: 0x5e8b3f, roughness: 0.9, flatShading: true });
  const tunicDarkMat = new THREE.MeshStandardMaterial({ name: 'tunicDark', color: 0x42612c, roughness: 0.9, flatShading: true });
  const pantsMat = new THREE.MeshStandardMaterial({ name: 'pants', color: 0x7b6248, roughness: 0.9, flatShading: true });
  const shoeMat = new THREE.MeshStandardMaterial({ name: 'shoe', color: 0x4a3326, roughness: 0.8, flatShading: true });
  const clothMat = new THREE.MeshStandardMaterial({ name: 'cloth', color: 0xefe6d2, roughness: 0.95, flatShading: true });
  const strawMat = new THREE.MeshStandardMaterial({ name: 'straw', color: 0xe2c06a, roughness: 0.9, flatShading: true });
  const eyeMat = new THREE.MeshStandardMaterial({ name: 'eye', color: 0x231c20, roughness: 0.35, flatShading: true });
  const blushMat = new THREE.MeshStandardMaterial({ name: 'blush', color: 0xe88a7e, roughness: 0.7, flatShading: true });

  // Shared geometries
  const boxGeo = new THREE.BoxGeometry(0.19, 0.52, 0.21);
  const boxGeo2 = new THREE.BoxGeometry(0.202, 0.035, 0.222);
  const boxGeo3 = new THREE.BoxGeometry(0.2, 0.09, 0.27);
  const boxGeo4 = new THREE.BoxGeometry(0.035, 0.012, 0.282);
  const boxGeo5 = new THREE.BoxGeometry(0.16, 0.2, 0.19);
  const boxGeo6 = new THREE.BoxGeometry(0.17, 0.05, 0.2);
  const boxGeo7 = new THREE.BoxGeometry(0.14, 0.26, 0.16);
  const boxGeo8 = new THREE.BoxGeometry(0.05, 0.16, 0.264);
  const boxGeo9 = new THREE.BoxGeometry(0.06, 0.08, 0.01);
  const boxGeo10 = new THREE.BoxGeometry(0.0228, 0.0228, 0.006);
  const boxGeo11 = new THREE.BoxGeometry(0.07, 0.035, 0.006);
  const boxGeo12 = new THREE.BoxGeometry(0.04, 0.025, 0.01);

  // Parts
  const legL = new THREE.Group();
  legL.name = 'legL';
  legL.position.set(0.105, 0.6, 0);
  root.add(legL);
  const trouserL = new THREE.Mesh(boxGeo, pantsMat);
  trouserL.name = 'trouserL';
  trouserL.position.set(0, -0.26, 0);
  legL.add(trouserL);
  const wrapL1 = new THREE.Mesh(boxGeo2, clothMat);
  wrapL1.name = 'wrapL1';
  wrapL1.position.set(0, -0.36, 0);
  wrapL1.rotation.set(0, 0, deg(-4));
  legL.add(wrapL1);
  const wrapL2 = new THREE.Mesh(boxGeo2, clothMat);
  wrapL2.name = 'wrapL2';
  wrapL2.position.set(0, -0.44, 0);
  wrapL2.rotation.set(0, 0, deg(4));
  legL.add(wrapL2);
  const shoeL = new THREE.Mesh(boxGeo3, shoeMat);
  shoeL.name = 'shoeL';
  shoeL.position.set(0, -0.555, 0.025);
  legL.add(shoeL);
  const legR = new THREE.Group();
  legR.name = 'legR';
  legR.position.set(-0.105, 0.6, 0);
  root.add(legR);
  const trouserR = new THREE.Mesh(boxGeo, pantsMat);
  trouserR.name = 'trouserR';
  trouserR.position.set(0, -0.26, 0);
  legR.add(trouserR);
  const wrapR1 = new THREE.Mesh(boxGeo2, clothMat);
  wrapR1.name = 'wrapR1';
  wrapR1.position.set(0, -0.36, 0);
  wrapR1.rotation.set(0, 0, deg(4));
  legR.add(wrapR1);
  const wrapR2 = new THREE.Mesh(boxGeo2, clothMat);
  wrapR2.name = 'wrapR2';
  wrapR2.position.set(0, -0.44, 0);
  wrapR2.rotation.set(0, 0, deg(-4));
  legR.add(wrapR2);
  const shoeR = new THREE.Mesh(boxGeo3, shoeMat);
  shoeR.name = 'shoeR';
  shoeR.position.set(0, -0.555, 0.025);
  legR.add(shoeR);
  const body = new THREE.Group();
  body.name = 'body';
  root.add(body);
  const tunic = new THREE.Mesh(new THREE.BoxGeometry(0.46, 0.53, 0.27), tunicMat);
  tunic.name = 'tunic';
  tunic.position.set(0, 0.855, 0);
  body.add(tunic);
  const tunicSkirt = new THREE.Mesh(new THREE.CylinderGeometry(0.3323, 0.3748, 0.2, 4, 1, false, Math.PI / 4), tunicMat);
  tunicSkirt.name = 'tunicSkirt';
  tunicSkirt.position.set(0, 0.5, 0);
  tunicSkirt.scale.set(1, 1, 0.5957);
  body.add(tunicSkirt);
  const tunicHem = new THREE.Mesh(new THREE.CylinderGeometry(0.3762, 0.3818, 0.03, 4, 1, false, Math.PI / 4), tunicDarkMat);
  tunicHem.name = 'tunicHem';
  tunicHem.position.set(0, 0.415, 0);
  tunicHem.scale.set(1, 1, 0.6042);
  body.add(tunicHem);
  const neckline = new THREE.Mesh(extrude([[-0.07, 0], [0.07, 0], [0, -0.11]], 0.01), skinMat);
  neckline.name = 'neckline';
  neckline.position.set(0, 1.115, 0.137);
  body.add(neckline);
  const lace = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.012, 0.01), tunicDarkMat);
  lace.name = 'lace';
  lace.position.set(0, 1.05, 0.141);
  body.add(lace);
  const ropeBelt = new THREE.Mesh(new THREE.BoxGeometry(0.472, 0.04, 0.282), strawMat);
  ropeBelt.name = 'ropeBelt';
  ropeBelt.position.set(0, 0.64, 0);
  body.add(ropeBelt);
  const ropeEnd = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.12, 0.02), strawMat);
  ropeEnd.name = 'ropeEnd';
  ropeEnd.position.set(-0.13, 0.57, 0.151);
  ropeEnd.rotation.set(0, 0, deg(-8));
  body.add(ropeEnd);
  const apron = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, 0.012), clothMat);
  apron.name = 'apron';
  apron.position.set(0, 0.5, 0.169);
  body.add(apron);
  const apronPocket = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.08, 0.008), tunicDarkMat);
  apronPocket.name = 'apronPocket';
  apronPocket.position.set(0.05, 0.48, 0.177);
  body.add(apronPocket);
  const apronBib = new THREE.Mesh(new THREE.BoxGeometry(0.22, 0.24, 0.012), clothMat);
  apronBib.name = 'apronBib';
  apronBib.position.set(0, 0.77, 0.138);
  body.add(apronBib);
  const apronStrapL = new THREE.Mesh(boxGeo4, clothMat);
  apronStrapL.name = 'apronStrapL';
  apronStrapL.position.set(0.1, 1.121, 0);
  body.add(apronStrapL);
  const apronStrapR = new THREE.Mesh(boxGeo4, clothMat);
  apronStrapR.name = 'apronStrapR';
  apronStrapR.position.set(-0.1, 1.121, 0);
  body.add(apronStrapR);
  const armL = new THREE.Group();
  armL.name = 'armL';
  armL.position.set(0.31, 1.08, 0);
  armL.rotation.set(0, 0, deg(6));
  root.add(armL);
  const sleeveL = new THREE.Mesh(boxGeo5, tunicMat);
  sleeveL.name = 'sleeveL';
  sleeveL.position.set(0, -0.085, 0);
  armL.add(sleeveL);
  const sleeveRollL = new THREE.Mesh(boxGeo6, tunicDarkMat);
  sleeveRollL.name = 'sleeveRollL';
  sleeveRollL.position.set(0, -0.18, 0);
  armL.add(sleeveRollL);
  const forearmL = new THREE.Mesh(boxGeo7, skinMat);
  forearmL.name = 'forearmL';
  forearmL.position.set(0, -0.32, 0);
  armL.add(forearmL);
  const armR = new THREE.Group();
  armR.name = 'armR';
  armR.position.set(-0.31, 1.08, 0);
  armR.rotation.set(0, 0, deg(-6));
  root.add(armR);
  const sleeveR = new THREE.Mesh(boxGeo5, tunicMat);
  sleeveR.name = 'sleeveR';
  sleeveR.position.set(0, -0.085, 0);
  armR.add(sleeveR);
  const sleeveRollR = new THREE.Mesh(boxGeo6, tunicDarkMat);
  sleeveRollR.name = 'sleeveRollR';
  sleeveRollR.position.set(0, -0.18, 0);
  armR.add(sleeveRollR);
  const forearmR = new THREE.Mesh(boxGeo7, skinMat);
  forearmR.name = 'forearmR';
  forearmR.position.set(0, -0.32, 0);
  armR.add(forearmR);
  const head = new THREE.Group();
  head.name = 'head';
  head.position.set(0, 1.1, 0);
  root.add(head);
  const neck = new THREE.Mesh(new THREE.BoxGeometry(0.16, 0.06, 0.16), skinMat);
  neck.name = 'neck';
  neck.position.set(0, 0.02, 0);
  head.add(neck);
  const skull = new THREE.Mesh(new THREE.BoxGeometry(0.48, 0.48, 0.48), skinMat);
  skull.name = 'skull';
  skull.position.set(0, 0.27, 0);
  head.add(skull);
  const hairTop = new THREE.Mesh(new THREE.BoxGeometry(0.492, 0.12, 0.492), hairMat);
  hairTop.name = 'hairTop';
  hairTop.position.set(0, 0.456, 0);
  head.add(hairTop);
  const hairBack = new THREE.Mesh(new THREE.BoxGeometry(0.492, 0.32, 0.12), hairMat);
  hairBack.name = 'hairBack';
  hairBack.position.set(0, 0.31, -0.186);
  head.add(hairBack);
  const sideburnL = new THREE.Mesh(boxGeo8, hairMat);
  sideburnL.name = 'sideburnL';
  sideburnL.position.set(0.221, 0.34, -0.1056);
  head.add(sideburnL);
  const sideburnR = new THREE.Mesh(boxGeo8, hairMat);
  sideburnR.name = 'sideburnR';
  sideburnR.position.set(-0.221, 0.34, -0.1056);
  head.add(sideburnR);
  const fringeL = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.07, 0.03), hairMat);
  fringeL.name = 'fringeL';
  fringeL.position.set(0.1, 0.38, 0.248);
  fringeL.rotation.set(0, 0, deg(8));
  head.add(fringeL);
  const fringeR = new THREE.Mesh(new THREE.BoxGeometry(0.16, 0.06, 0.03), hairMat);
  fringeR.name = 'fringeR';
  fringeR.position.set(-0.12, 0.385, 0.248);
  fringeR.rotation.set(0, 0, deg(-6));
  head.add(fringeR);
  const eyeL = new THREE.Mesh(boxGeo9, eyeMat);
  eyeL.name = 'eyeL';
  eyeL.position.set(0.1, 0.29, 0.243);
  head.add(eyeL);
  const eyeShineL = new THREE.Mesh(boxGeo10, clothMat);
  eyeShineL.name = 'eyeShineL';
  eyeShineL.position.set(0.1084, 0.3076, 0.249);
  head.add(eyeShineL);
  const cheekL = new THREE.Mesh(boxGeo11, blushMat);
  cheekL.name = 'cheekL';
  cheekL.position.set(0.155, 0.21, 0.243);
  head.add(cheekL);
  const eyeR = new THREE.Mesh(boxGeo9, eyeMat);
  eyeR.name = 'eyeR';
  eyeR.position.set(-0.1, 0.29, 0.243);
  head.add(eyeR);
  const eyeShineR = new THREE.Mesh(boxGeo10, clothMat);
  eyeShineR.name = 'eyeShineR';
  eyeShineR.position.set(-0.1084, 0.3076, 0.249);
  head.add(eyeShineR);
  const cheekR = new THREE.Mesh(boxGeo11, blushMat);
  cheekR.name = 'cheekR';
  cheekR.position.set(-0.155, 0.21, 0.243);
  head.add(cheekR);
  const nose = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.05, 0.04), skinMat);
  nose.name = 'nose';
  nose.position.set(0, 0.23, 0.26);
  head.add(nose);
  const mouth = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.025, 0.01), blushMat);
  mouth.name = 'mouth';
  mouth.position.set(0, 0.145, 0.243);
  head.add(mouth);
  const smileL = new THREE.Mesh(boxGeo12, blushMat);
  smileL.name = 'smileL';
  smileL.position.set(0.06, 0.155, 0.243);
  smileL.rotation.set(0, 0, deg(25));
  head.add(smileL);
  const smileR = new THREE.Mesh(boxGeo12, blushMat);
  smileR.name = 'smileR';
  smileR.position.set(-0.06, 0.155, 0.243);
  smileR.rotation.set(0, 0, deg(-25));
  head.add(smileR);
  const strawHat = new THREE.Group();
  strawHat.name = 'strawHat';
  strawHat.position.set(0, 0.475, 0);
  strawHat.rotation.set(deg(-4), 0, deg(3));
  head.add(strawHat);
  const hatBrim = new THREE.Mesh(new THREE.CylinderGeometry(0.44, 0.44, 0.03, 10, 1, false, 0.3142), strawMat);
  hatBrim.name = 'hatBrim';
  hatBrim.position.set(0, 0.015, 0);
  strawHat.add(hatBrim);
  const hatCrown = new THREE.Mesh(new THREE.CylinderGeometry(0.2828, 0.3606, 0.19, 4, 1, false, Math.PI / 4), strawMat);
  hatCrown.name = 'hatCrown';
  hatCrown.position.set(0, 0.125, 0);
  strawHat.add(hatCrown);
  const hatBand = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.3705, 0.05, 4, 1, false, Math.PI / 4), tunicDarkMat);
  hatBand.name = 'hatBand';
  hatBand.position.set(0, 0.055, 0);
  strawHat.add(hatBand);

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

export default createVillager;

Ratings & comments

—
No ratings yet — be the first.
5
0
4
0
3
0
2
0
1
0

Sign in to rate and comment.

0 comments

No comments yet. Share how you used this model or what could be better.

Related models

Same category or similar tags