Codels
1.5K tris4.62 × 6.62 × 4.37 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.
/**
 * Radar Dish — Codels 3D model library
 * License: CC0-1.0 · Units: meters · Y-up · Front: +Z · Origin: center of the base (y = 0)
 * Triangles: 1,532 · Size (W × H × D): 4.62 × 6.62 × 4.37 m
 *
 * Usage:
 *   import { createRadarDish } from './radar-dish.js';
 *   
 *   const model = createRadarDish();
 *   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 createRadarDish() {
  const root = new THREE.Group();
  root.name = 'RadarDish';

  // Materials
  const dishMat = new THREE.MeshStandardMaterial({ name: 'dish', color: 0xe9ecf0, roughness: 0.5, metalness: 0.15, flatShading: true });
  const metalMat = new THREE.MeshStandardMaterial({ name: 'metal', color: 0x7d858f, roughness: 0.45, metalness: 0.6, flatShading: true });
  const concreteMat = new THREE.MeshStandardMaterial({ name: 'concrete', color: 0xa9a49b, roughness: 0.95, flatShading: true });
  const accentMat = new THREE.MeshStandardMaterial({ name: 'accent', color: 0xe0683a, roughness: 0.5, metalness: 0.2, flatShading: true });
  const beaconMat = new THREE.MeshStandardMaterial({ name: 'beacon', color: 0xff3b30, roughness: 0.4, emissive: 0xff2a1f, emissiveIntensity: 1.5, flatShading: true });

  // Shared geometries
  const boxGeo = new THREE.BoxGeometry(0.16, 1.15, 0.7);

  // Parts
  const base = new THREE.Group();
  base.name = 'base';
  root.add(base);
  const plinth = new THREE.Mesh(new THREE.CylinderGeometry(1.55, 1.75, 0.45, 8), concreteMat);
  plinth.name = 'plinth';
  plinth.position.set(0, 0.225, 0);
  plinth.rotation.set(0, deg(22.5), 0);
  base.add(plinth);
  const pedestal = new THREE.Mesh(new THREE.CylinderGeometry(0.62, 0.82, 2.3, 10), metalMat);
  pedestal.name = 'pedestal';
  pedestal.position.set(0, 1.6, 0);
  pedestal.rotation.set(0, deg(18), 0);
  base.add(pedestal);
  const pedestalRing = new THREE.Mesh(new THREE.CylinderGeometry(0.85, 0.85, 0.08, 10), accentMat);
  pedestalRing.name = 'pedestalRing';
  pedestalRing.position.set(0, 0.49, 0);
  base.add(pedestalRing);
  const door = new THREE.Mesh(new THREE.BoxGeometry(0.46, 0.9, 0.06), accentMat);
  door.name = 'door';
  door.position.set(0, 0.97, 0.752);
  door.rotation.set(deg(-4.5), 0, 0);
  base.add(door);
  const antennaMast = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.05, 0.95, 5), metalMat);
  antennaMast.name = 'antennaMast';
  antennaMast.position.set(-1.1, 0.925, -0.75);
  base.add(antennaMast);
  const antennaDipole = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.03, 0.03), metalMat);
  antennaDipole.name = 'antennaDipole';
  antennaDipole.position.set(-1.1, 1.2, -0.75);
  antennaDipole.rotation.set(0, deg(35), 0);
  base.add(antennaDipole);
  const antennaBeacon = new THREE.Mesh(new THREE.SphereGeometry(0.07, 8, 6), beaconMat);
  antennaBeacon.name = 'antennaBeacon';
  antennaBeacon.position.set(-1.1, 1.45, -0.75);
  base.add(antennaBeacon);
  const head = new THREE.Group();
  head.name = 'head';
  head.rotation.set(0, deg(-25), 0);
  root.add(head);
  const turntable = new THREE.Mesh(new THREE.CylinderGeometry(0.95, 0.95, 0.3, 14), accentMat);
  turntable.name = 'turntable';
  turntable.position.set(0, 2.9, 0);
  head.add(turntable);
  const yokeArmL = new THREE.Mesh(boxGeo, metalMat);
  yokeArmL.name = 'yokeArmL';
  yokeArmL.position.set(0.55, 3.625, 0);
  head.add(yokeArmL);
  const yokeArmR = new THREE.Mesh(boxGeo, metalMat);
  yokeArmR.name = 'yokeArmR';
  yokeArmR.position.set(-0.55, 3.625, 0);
  head.add(yokeArmR);
  const axle = new THREE.Mesh(new THREE.CylinderGeometry(0.14, 0.14, 1.36, 10), metalMat);
  axle.name = 'axle';
  axle.position.set(0, 3.75, 0);
  axle.rotation.set(0, 0, deg(90));
  head.add(axle);
  const dish = new THREE.Group();
  dish.name = 'dish';
  dish.position.set(0, 3.75, 0);
  dish.rotation.set(deg(55), 0, 0);
  head.add(dish);
  const dishHub = new THREE.Mesh(new THREE.CylinderGeometry(0.32, 0.42, 0.46, 10), metalMat);
  dishHub.name = 'dishHub';
  dishHub.position.set(0, 0.23, 0);
  dish.add(dishHub);
  const dishShell = new THREE.Mesh(lathe([[0.001, 0.42], [0.4, 0.4476], [0.8, 0.5303], [1.2, 0.6683], [1.6, 0.8614], [2, 1.1097], [2.4, 1.4131], [2.45, 1.4611], [2.4, 1.4931], [2, 1.1897], [1.6, 0.9414], [1.2, 0.7483], [0.8, 0.6103], [0.4, 0.5276], [0.001, 0.5]], 20), dishMat);
  dishShell.name = 'dishShell';
  dish.add(dishShell);
  const torus17 = new THREE.Mesh(new THREE.TorusGeometry(0.9, 0.022, 3, 20), metalMat);
  torus17.position.set(0, 0.6517, 0);
  torus17.rotation.set(deg(90), 0, 0);
  dish.add(torus17);
  const torus18 = new THREE.Mesh(new THREE.TorusGeometry(1.65, 0.022, 3, 20), metalMat);
  torus18.position.set(0, 0.9814, 0);
  torus18.rotation.set(deg(90), 0, 0);
  dish.add(torus18);
  const dishRim = new THREE.Mesh(new THREE.TorusGeometry(2.42, 0.045, 4, 20), accentMat);
  dishRim.name = 'dishRim';
  dishRim.position.set(0, 1.4831, 0);
  dishRim.rotation.set(deg(90), 0, 0);
  dish.add(dishRim);
  const tube20 = new THREE.Mesh(tube([[2.208, 1.3406, 0], [0, 1.85, 0]], 0.03, 1, 4), metalMat);
  dish.add(tube20);
  const tube21 = new THREE.Mesh(tube([[-1.104, 1.3406, -1.9122], [0, 1.85, 0]], 0.03, 1, 4), metalMat);
  dish.add(tube21);
  const tube22 = new THREE.Mesh(tube([[-1.104, 1.3406, 1.9122], [0, 1.85, 0]], 0.03, 1, 4), metalMat);
  dish.add(tube22);
  const feedHorn = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.2, 0.36, 10), accentMat);
  feedHorn.name = 'feedHorn';
  feedHorn.position.set(0, 1.95, 0);
  dish.add(feedHorn);
  const cylinder24 = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 0.12, 8), metalMat);
  cylinder24.position.set(0, 2.19, 0);
  dish.add(cylinder24);
  const beacon = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 6), beaconMat);
  beacon.name = 'beacon';
  beacon.position.set(0, 2.31, 0);
  dish.add(beacon);

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

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

export default createRadarDish;

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