<!-- SAGA2 · part 38 -->
</span> faces</div>
  <button onclick="generate(100)">+100 Faces</button>
  <button onclick="clearAll()">Clear</button>
</div>
<script>
// Face builder (parametric, from v160 template, proportion-corrected)
function buildFace(seed) {
  // Seeded random
  var s = seed;
  function rnd() { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; }
  
  var g = new THREE.Group();
  var iC = new THREE.MeshBasicMaterial({color:0x00cccc, transparent:true, opacity:0.5, side:THREE.DoubleSide});
  var iG = new THREE.MeshBasicMaterial({color:0xffcc44, transparent:true, opacity:0.5, side:THREE.DoubleSide});
  var iD = new THREE.MeshBasicMaterial({color:0x008888, transparent:true, opacity:0.3, side:THREE.DoubleSide});
  
  function ribbon(pts, w, mat) {
    var vs = [], idx = [];
    for(var i=0;i<pts.length;i++) {
      var p = pts[i];
      var dir = new THREE.Vector3();
      if(i==0) dir.set(pts[1][0]-p[0], pts[1][1]-p[1], pts[1][2]-p[2]);
      else if(i==pts.length-1) dir.set(p[0]-pts[i-1][0], p[1]-pts[i-1][1], p[2]-pts[i-1][2]);
      else dir.set(pts[i+1][0]-pts[i-1][0], pts[i+1][1]-pts[i-1][1], pts[i+1][2]-pts[i-1][2]);
      dir.normalize();
      var up = new THREE.Vector3(0,0,1);
      var side = new THREE.Vector3().crossVectors(dir, up).normalize().multiplyScalar(w/2);
      vs.push(p[0]-side.x, p[1]-side.y, p[2]-side.z, p[0]+side.x, p[1]+side.y, p[2]+side.z);
      if(i>0) { var b=i*2; idx.push(b-2,b-1,b, b-1,b+1,b); }
    }
    var geo = new THREE.BufferGeometry();
    geo.setAttribute('position', new THREE.Float32BufferAttribute(vs, 3));
    geo.setIndex(idx);
    return new THREE.Mesh(geo, mat);
  }
  
  function L(pts, mat) { g.add(ribbon(pts, 0.06, mat)); }
  
  // === PARAMS (proportion-based, with variation) ===
  var eyeW = 1.2 + rnd()*0.4;  // eye width
  var asym = rnd()*0.3;         // asymmetry
  var jawMul = 0.85 + rnd()*0.3; // jaw width variation
  var noseMul = 0.85 + rnd()*0.3; // nose width variation
  
  // Positions (eye line at y=0, following thirds/fifths)
  var eY = 0;
  var eLx = -1.5*eyeW*(1+(rnd()-0.5)*asym);
  var eRx = 1.5*eyeW*(1+(rnd()-0.5)*asym);
  var browY = 1.0*eyeW;
  var noseY = -1.5*eyeW;
  var chinY = -4.0*eyeW;
  var mouthY = -2.33*eyeW;
  var mouthHW = 1.5*eyeW;
  var noseHW = 1.0*eyeW*noseMul;
  var jawHW = 2.2*eyeW*jawMul;
  
  // Cranium (simplified)
  for(var i=0;i<12;i++) {
    var a=(i/12)*Math.PI*2, pts=[];
    for(var j=0;j<=12;j++) {
      var ph=(j/12)*Math.PI, r=3.2*eyeW*Math.sin(ph);
      pts.push([Math.cos(a)*r, 3.2*eyeW*Math.cos(ph)+1.5*eyeW, Math.sin(a)*r*0.92]);
    }
    L(pts, i%3==0?iG:iC);
  }
  
  // Eyes (almond, iris, pupil)
  function eye(cx) {
    var ao=[];
    for(var k=0;k<=20;k++) {
      var an=(k/20)*Math.PI*2;
      var ax=Math.cos(an), ay=Math.sin(an);
      var pinch = ax<0?0.75:1.0;
      ao.push([cx+ax*eyeW*0.7*pinch, eY+ay*eyeW*0.32*(ax<0?0.8:1.0), 2.9*eyeW]);
    }
    L(ao, iG);
    // Iris
    for(var ir=0;ir<2;ir++) {
      var ip=[], ird=eyeW*(0.28-ir*0.08);
      for(var ia=0;ia<=16;ia++) {
        var ian=(ia/16)*Math.PI*2;
        ip.push([cx+Math.cos(ian)*ird, eY+Math.sin(ian)*ird, 2.95*eyeW]);
      }
      L(ip, iC);
    }
    // Pupil
    var pp=[];
    for(var pa=0;pa<=10;pa++) {
      var pan=(pa/10)*Math.PI*2;
      pp.push([cx+Math.cos(pan)*eyeW*0.09, eY+Math.sin(pan)*eyeW*0.09, 2.97*eyeW]);
    }
    L(pp, iD);
  }
  eye(eLx); eye(eRx);
  
  // Nose
  L([[0,browY,3.0*eyeW],[0,noseY+0.5*eyeW,3.2*eyeW],[0,noseY,3.3*eyeW]], iG); // bridge
  L([[-noseHW,noseY,3.2*eyeW],[-noseHW*0.7,noseY-0.2*eyeW,3.3*eyeW]], iD); // left nostril
  L([[noseHW,noseY,3.2*eyeW],[noseHW*0.7,noseY-0.2*eyeW,3.3*eyeW]], iD); // right nostril
  
  // Mouth
  L([[-mouthHW,mouthY,2.9*eyeW],[0,mouthY+0.1*eyeW,3.0*eyeW],[mouthHW,mouthY,2.9*eyeW]], iG);
  
  // Jaw
  L([[-jawHW,chinY+1.5*eyeW,2.5*eyeW],[-jawHW*0.6,chinY+0.5*eyeW,2.8*eyeW],[0,chinY,2.9*eyeW]], iG);
  L([[jawHW,chinY+1.5*eyeW,2.5*eyeW],[jawHW*0.6,chinY+0.5*eyeW,2.8*eyeW],[0,chinY,2.9*eyeW]], iG);
  
  return g;
}

// Scene
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.1, 1000);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
camera.position.set(0, -2, 60);
camera.lookAt(0, -2, -30);

var faces = [];
var seedBase = 1;

function generate(n) {
  for(var i=0;i<n;i++) {
    var f = buildFace(seedBase++);
    // Grid layout
    var idx = faces.length;
    var cols = 10;
    var x = (idx % cols - cols/2) * 12;
    var z = -Math.floor(idx / cols) * 14;
    f.position.set(x, 0, z);
    scene.add(f);
    faces.push(f);
  }
  document.getElementById('count').textContent = faces.length;
  // Camera: front view of grid, fit all
  var cols = 10;
  var rows = Math.ceil(faces.length / cols);
  var gridW = cols * 12, gridH = rows * 14;
  var dist = Math.max(gridW, gridH) * 0.9;
  camera.position.set(0, -2, dist);
  camera.lookAt(0,