Codels
삼각형 3.4천개7.33 × 3.38 × 7.33 m

코드에서 사용하기

에디터에서 리믹스
독립형 ES 모듈이에요 — three에만 의존해요. 팩토리 함수를 호출하고 그룹을 씬에 추가하세요.
/**
 * UFO Flying Saucer — Codels 3D model library
 * License: CC0-1.0 · Units: meters · Y-up · Front: +Z · Origin: center of the base (y = 0)
 * Triangles: 3,422 · Size (W × H × D): 7.33 × 3.38 × 7.33 m
 *
 * Usage:
 *   import { createUfo } from './ufo.js';
 *   
 *   const model = createUfo();
 *   model.position.set(0, 0, 0);
 *   scene.add(model);
 *   
 *   // Animations: idle
 *   const mixer = new THREE.AnimationMixer(model);
 *   mixer.clipAction(THREE.AnimationClip.findByName(model.animations, 'idle')).play();
 *   // in your render loop: mixer.update(clock.getDelta());
 */
import * as THREE from 'three';

const deg = THREE.MathUtils.degToRad;

// Revolve a 2D profile [[radius, y], ...] around the Y axis.
function lathe(points, segments, phiStart = 0, phiLength = Math.PI * 2) {
  return new THREE.LatheGeometry(points.map(([x, y]) => new THREE.Vector2(x, y)), segments, phiStart, phiLength);
}

// Tube along a smooth curve through the given points.
function tube(path, radius, tubularSegments, radialSegments, closed = false) {
  const curve = new THREE.CatmullRomCurve3(path.map(([x, y, z]) => new THREE.Vector3(x, y, z)), closed);
  return new THREE.TubeGeometry(curve, tubularSegments, radius, radialSegments, closed);
}

// Build animation clips from simple descriptions (Codels). Play them with THREE.AnimationMixer.
function codelsClips(THREE, entries) {
  var TAU = Math.PI * 2;
  var DEG = Math.PI / 180;
  var AX = { x: new THREE.Vector3(1, 0, 0), y: new THREE.Vector3(0, 1, 0), z: new THREE.Vector3(0, 0, 1) };
  var periodOf = function (a) { return a.type === 'spin' ? 360 / Math.abs(a.speed) : a.period; };
  var clips = [];

  // loops → 'idle'
  var loops = [];
  entries.forEach(function (e) {
    var l = e.anim.filter(function (a) { return a.type !== 'hinge'; });
    if (l.length) loops.push({ target: e.target, anim: l });
  });
  if (loops.length) {
    var periods = [];
    loops.forEach(function (e) { e.anim.forEach(function (a) { periods.push(periodOf(a)); }); });
    var longest = Math.max.apply(null, periods);
    var D = longest;
    var bestErr = Infinity;
    for (var m = 1; m <= 6 && bestErr > 0.01; m++) {
      var err = 0;
      periods.forEach(function (p) {
        var n = Math.max(1, Math.round((longest * m) / p));
        err = Math.max(err, Math.abs((longest * m) / n - p) / p);
      });
      if (err < bestErr - 1e-9) { bestErr = err; D = longest * m; }
    }
    var cycles = function (a) { return Math.max(1, Math.round(D / periodOf(a))); };
    var steps = 24;
    loops.forEach(function (e) { e.anim.forEach(function (a) { steps = Math.max(steps, cycles(a) * 24); }); });
    steps = Math.min(steps, 720);
    var tracks = [];
    loops.forEach(function (e) {
      var o = e.target;
      var q0 = o.quaternion.clone(), p0 = o.position.clone(), s0 = o.scale.clone();
      var q = new THREE.Quaternion(), turn = new THREE.Quaternion(), p = new THREE.Vector3(), s = new THREE.Vector3();
      var has = function (type) { return e.anim.some(function (a) { return a.type === type; }); };
      var turns = has('spin') || has('swing'), moves = has('bob'), grows = has('pulse');
      var times = [], qv = [], pv = [], sv = [];
      for (var k = 0; k <= steps; k++) {
        var t = (D * k) / steps;
        q.copy(q0); p.copy(p0); s.copy(s0);
        e.anim.forEach(function (a) {
          var n = cycles(a);
          var w = Math.sin(TAU * ((n * t) / D + (a.phase || 0)));
          if (a.type === 'spin') q.multiply(turn.setFromAxisAngle(AX[a.axis], (a.speed < 0 ? -TAU : TAU) * ((n * t) / D)));
          else if (a.type === 'swing') q.multiply(turn.setFromAxisAngle(AX[a.axis], a.angle * DEG * w));
          else if (a.type === 'bob') p.addScaledVector(AX[a.axis], a.distance * w);
          else if (a.type === 'pulse') s.multiplyScalar(1 + a.amount * w);
        });
        times.push(t);
        if (turns) qv.push(q.x, q.y, q.z, q.w);
        if (moves) pv.push(p.x, p.y, p.z);
        if (grows) sv.push(s.x, s.y, s.z);
      }
      if (turns) tracks.push(new THREE.QuaternionKeyframeTrack(o.uuid + '.quaternion', times, qv));
      if (moves) tracks.push(new THREE.VectorKeyframeTrack(o.uuid + '.position', times, pv));
      if (grows) tracks.push(new THREE.VectorKeyframeTrack(o.uuid + '.scale', times, sv));
    });
    clips.push(new THREE.AnimationClip('idle', D, tracks));
  }

  // hinge moves → one-shot clips ('open' …)
  var shots = {};
  entries.forEach(function (e) {
    e.anim.forEach(function (a) {
      if (a.type !== 'hinge') return;
      var name = a.clip || 'open';
      (shots[name] = shots[name] || []).push({ target: e.target, a: a });
    });
  });
  Object.keys(shots).forEach(function (name) {
    var tracks = [];
    var length = 0;
    shots[name].forEach(function (h) {
      var o = h.target, q0 = o.quaternion.clone(), q = new THREE.Quaternion(), turn = new THREE.Quaternion();
      var n = Math.max(4, Math.ceil(Math.abs(h.a.angle) / 10));
      var times = [], qv = [];
      for (var k = 0; k <= n; k++) {
        var f = k / n;
        var eased = f * f * (3 - 2 * f);
        q.copy(q0).multiply(turn.setFromAxisAngle(AX[h.a.axis], h.a.angle * DEG * eased));
        times.push(h.a.duration * f);
        qv.push(q.x, q.y, q.z, q.w);
      }
      length = Math.max(length, h.a.duration);
      tracks.push(new THREE.QuaternionKeyframeTrack(o.uuid + '.quaternion', times, qv));
    });
    clips.push(new THREE.AnimationClip(name, length, tracks));
  });
  return clips;
}

export function createUfo() {
  const root = new THREE.Group();
  root.name = 'Ufo';

  // Materials
  const hullMat = new THREE.MeshStandardMaterial({ name: 'hull', color: 0xb9c1cb, roughness: 0.3, metalness: 0.6, flatShading: true });
  const darkMat = new THREE.MeshStandardMaterial({ name: 'dark', color: 0x3d434c, roughness: 0.5, metalness: 0.5, flatShading: true });
  const domeMat = new THREE.MeshStandardMaterial({ name: 'dome', color: 0x6fc4d4, roughness: 0.12, metalness: 0.3, transparent: true, opacity: 0.6 });
  const glowMat = new THREE.MeshStandardMaterial({ name: 'glow', color: 0x3dff78, roughness: 0.4, emissive: 0x3dff78, emissiveIntensity: 1.5, flatShading: true });
  const alienMat = new THREE.MeshStandardMaterial({ name: 'alien', color: 0x8bd05a, roughness: 0.6, flatShading: true });

  // Shared geometries
  const sphereGeo = new THREE.SphereGeometry(0.14, 8, 6);
  const boxGeo = new THREE.BoxGeometry(0.5, 0.06, 0.22);
  const sphereGeo2 = new THREE.SphereGeometry(0.13, 8, 6);
  const cylinderGeo = new THREE.CylinderGeometry(0.02, 0.02, 0.3, 4);
  const sphereGeo3 = new THREE.SphereGeometry(0.06, 6, 4);
  const cylinderGeo2 = new THREE.CylinderGeometry(0.3, 0.36, 0.12, 10);

  // Parts
  const saucer = new THREE.Group();
  saucer.name = 'saucer';
  root.add(saucer);
  const lowerHull = new THREE.Mesh(lathe([[0, 0.72], [0.7, 0.74], [1.6, 0.84], [2.6, 1], [3.3, 1.18], [3.55, 1.32]], 24), hullMat);
  lowerHull.name = 'lowerHull';
  saucer.add(lowerHull);
  const upperHull = new THREE.Mesh(lathe([[3.55, 1.32], [3.35, 1.46], [2.7, 1.66], [1.9, 1.84], [1.45, 1.9], [0, 1.92]], 24), hullMat);
  upperHull.name = 'upperHull';
  saucer.add(upperHull);
  const rimRing = new THREE.Mesh(new THREE.TorusGeometry(3.55, 0.11, 5, 24), darkMat);
  rimRing.name = 'rimRing';
  rimRing.position.set(0, 1.32, 0);
  rimRing.rotation.set(deg(90), 0, 0);
  saucer.add(rimRing);
  const panelRing = new THREE.Mesh(new THREE.TorusGeometry(2.25, 0.05, 4, 24), darkMat);
  panelRing.name = 'panelRing';
  panelRing.position.set(0, 1.765, 0);
  panelRing.rotation.set(deg(90), 0, 0);
  saucer.add(panelRing);
  const domeCollar = new THREE.Mesh(new THREE.CylinderGeometry(1.5, 1.55, 0.12, 24), darkMat);
  domeCollar.name = 'domeCollar';
  domeCollar.position.set(0, 1.92, 0);
  saucer.add(domeCollar);
  const rimLight1 = new THREE.Mesh(sphereGeo, glowMat);
  rimLight1.name = 'rimLight1';
  rimLight1.position.set(0.9447, 1.32, 3.5256);
  saucer.add(rimLight1);
  const rimLight2 = new THREE.Mesh(sphereGeo, glowMat);
  rimLight2.name = 'rimLight2';
  rimLight2.position.set(2.5809, 1.32, 2.5809);
  saucer.add(rimLight2);
  const rimLight3 = new THREE.Mesh(sphereGeo, glowMat);
  rimLight3.name = 'rimLight3';
  rimLight3.position.set(3.5256, 1.32, 0.9447);
  saucer.add(rimLight3);
  const rimLight4 = new THREE.Mesh(sphereGeo, glowMat);
  rimLight4.name = 'rimLight4';
  rimLight4.position.set(3.5256, 1.32, -0.9447);
  saucer.add(rimLight4);
  const rimLight5 = new THREE.Mesh(sphereGeo, glowMat);
  rimLight5.name = 'rimLight5';
  rimLight5.position.set(2.5809, 1.32, -2.5809);
  saucer.add(rimLight5);
  const rimLight6 = new THREE.Mesh(sphereGeo, glowMat);
  rimLight6.name = 'rimLight6';
  rimLight6.position.set(0.9447, 1.32, -3.5256);
  saucer.add(rimLight6);
  const rimLight7 = new THREE.Mesh(sphereGeo, glowMat);
  rimLight7.name = 'rimLight7';
  rimLight7.position.set(-0.9447, 1.32, -3.5256);
  saucer.add(rimLight7);
  const rimLight8 = new THREE.Mesh(sphereGeo, glowMat);
  rimLight8.name = 'rimLight8';
  rimLight8.position.set(-2.5809, 1.32, -2.5809);
  saucer.add(rimLight8);
  const rimLight9 = new THREE.Mesh(sphereGeo, glowMat);
  rimLight9.name = 'rimLight9';
  rimLight9.position.set(-3.5256, 1.32, -0.9447);
  saucer.add(rimLight9);
  const rimLight10 = new THREE.Mesh(sphereGeo, glowMat);
  rimLight10.name = 'rimLight10';
  rimLight10.position.set(-3.5256, 1.32, 0.9447);
  saucer.add(rimLight10);
  const rimLight11 = new THREE.Mesh(sphereGeo, glowMat);
  rimLight11.name = 'rimLight11';
  rimLight11.position.set(-2.5809, 1.32, 2.5809);
  saucer.add(rimLight11);
  const rimLight12 = new THREE.Mesh(sphereGeo, glowMat);
  rimLight12.name = 'rimLight12';
  rimLight12.position.set(-0.9447, 1.32, 3.5256);
  saucer.add(rimLight12);
  const box19 = new THREE.Mesh(boxGeo, darkMat);
  box19.position.set(0, 1.6, 2.95);
  box19.rotation.set(0, deg(90), deg(-14));
  saucer.add(box19);
  const box20 = new THREE.Mesh(boxGeo, darkMat);
  box20.position.set(2.086, 1.6, 2.086);
  box20.rotation.set(0, deg(135), deg(-14));
  saucer.add(box20);
  const box21 = new THREE.Mesh(boxGeo, darkMat);
  box21.position.set(2.95, 1.6, 0);
  box21.rotation.set(0, deg(180), deg(-14));
  saucer.add(box21);
  const box22 = new THREE.Mesh(boxGeo, darkMat);
  box22.position.set(2.086, 1.6, -2.086);
  box22.rotation.set(0, deg(225), deg(-14));
  saucer.add(box22);
  const box23 = new THREE.Mesh(boxGeo, darkMat);
  box23.position.set(0, 1.6, -2.95);
  box23.rotation.set(0, deg(270), deg(-14));
  saucer.add(box23);
  const box24 = new THREE.Mesh(boxGeo, darkMat);
  box24.position.set(-2.086, 1.6, -2.086);
  box24.rotation.set(0, deg(315), deg(-14));
  saucer.add(box24);
  const box25 = new THREE.Mesh(boxGeo, darkMat);
  box25.position.set(-2.95, 1.6, 0);
  box25.rotation.set(0, deg(360), deg(-14));
  saucer.add(box25);
  const box26 = new THREE.Mesh(boxGeo, darkMat);
  box26.position.set(-2.086, 1.6, 2.086);
  box26.rotation.set(0, deg(405), deg(-14));
  saucer.add(box26);
  const emitterHousing = new THREE.Mesh(new THREE.CylinderGeometry(0.75, 0.9, 0.16, 24), darkMat);
  emitterHousing.name = 'emitterHousing';
  emitterHousing.position.set(0, 0.72, 0);
  saucer.add(emitterHousing);
  const tractorBeam = new THREE.Mesh(new THREE.CylinderGeometry(0.55, 0.55, 0.02, 24), glowMat);
  tractorBeam.name = 'tractorBeam';
  tractorBeam.position.set(0, 0.635, 0);
  saucer.add(tractorBeam);
  const dome = new THREE.Mesh(new THREE.SphereGeometry(1.42, 18, 7, 0, Math.PI * 2, 0, Math.PI / 2), domeMat);
  dome.name = 'dome';
  dome.position.set(0, 1.96, 0);
  saucer.add(dome);
  const pilot = new THREE.Group();
  pilot.name = 'pilot';
  pilot.position.set(0, 1.9, 0.12);
  pilot.scale.setScalar(0.7);
  saucer.add(pilot);
  const alienBody = new THREE.Mesh(new THREE.CapsuleGeometry(0.28, 0.35, 3, 10), alienMat);
  alienBody.name = 'alienBody';
  alienBody.position.set(0, 0.455, 0);
  pilot.add(alienBody);
  const alienHead = new THREE.Mesh(new THREE.SphereGeometry(0.42, 12, 9), alienMat);
  alienHead.name = 'alienHead';
  alienHead.position.set(0, 1.25, 0);
  alienHead.scale.set(1, 1.12, 0.95);
  pilot.add(alienHead);
  const eyeL = new THREE.Mesh(sphereGeo2, darkMat);
  eyeL.name = 'eyeL';
  eyeL.position.set(0.17, 1.3, 0.34);
  eyeL.rotation.set(0, 0, deg(25));
  eyeL.scale.set(1, 1.4, 0.55);
  pilot.add(eyeL);
  const cylinder34 = new THREE.Mesh(cylinderGeo, alienMat);
  cylinder34.position.set(0.16, 1.78, 0);
  cylinder34.rotation.set(0, 0, deg(-18));
  pilot.add(cylinder34);
  const sphere35 = new THREE.Mesh(sphereGeo3, glowMat);
  sphere35.position.set(0.21, 1.93, 0);
  pilot.add(sphere35);
  const eyeR = new THREE.Mesh(sphereGeo2, darkMat);
  eyeR.name = 'eyeR';
  eyeR.position.set(-0.17, 1.3, 0.34);
  eyeR.rotation.set(0, 0, deg(-25));
  eyeR.scale.set(1, 1.4, 0.55);
  pilot.add(eyeR);
  const cylinder37 = new THREE.Mesh(cylinderGeo, alienMat);
  cylinder37.position.set(-0.16, 1.78, 0);
  cylinder37.rotation.set(0, 0, deg(18));
  pilot.add(cylinder37);
  const sphere38 = new THREE.Mesh(sphereGeo3, glowMat);
  sphere38.position.set(-0.21, 1.93, 0);
  pilot.add(sphere38);
  const legs = new THREE.Group();
  legs.name = 'legs';
  root.add(legs);
  const leg1 = new THREE.Mesh(tube([[1.3856, 0.9, 0.8], [1.9919, 0.5, 1.15], [2.2517, 0.12, 1.3]], 0.08, 6, 6), darkMat);
  leg1.name = 'leg1';
  legs.add(leg1);
  const footPad1 = new THREE.Mesh(cylinderGeo2, hullMat);
  footPad1.name = 'footPad1';
  footPad1.position.set(2.2517, 0.06, 1.3);
  legs.add(footPad1);
  const leg2 = new THREE.Mesh(tube([[0, 0.9, -1.6], [0, 0.5, -2.3], [0, 0.12, -2.6]], 0.08, 6, 6), darkMat);
  leg2.name = 'leg2';
  legs.add(leg2);
  const footPad2 = new THREE.Mesh(cylinderGeo2, hullMat);
  footPad2.name = 'footPad2';
  footPad2.position.set(0, 0.06, -2.6);
  legs.add(footPad2);
  const leg3 = new THREE.Mesh(tube([[-1.3856, 0.9, 0.8], [-1.9919, 0.5, 1.15], [-2.2517, 0.12, 1.3]], 0.08, 6, 6), darkMat);
  leg3.name = 'leg3';
  legs.add(leg3);
  const footPad3 = new THREE.Mesh(cylinderGeo2, hullMat);
  footPad3.name = 'footPad3';
  footPad3.position.set(-2.2517, 0.06, 1.3);
  legs.add(footPad3);

  // Animations ('idle') — play them with THREE.AnimationMixer
  root.animations = codelsClips(THREE, [
    { target: saucer, anim: [{ type: 'spin', axis: 'y', speed: 30 }] },
  ]);

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

export default createUfo;

평점 및 댓글

—
아직 평점이 없어요 — 첫 평점을 남겨 보세요.
5
0
4
0
3
0
2
0
1
0

평점과 댓글을 남기려면 로그인하세요.

댓글 0개

아직 댓글이 없어요. 이 모델을 어떻게 사용했는지, 어떤 점이 더 나아질 수 있을지 공유해 주세요.

관련 모델

같은 카테고리 또는 비슷한 태그