歯車について勉強する の履歴(No.5)

更新


公開メモ

ちょっと思い立って歯車について勉強してみました

成果物

一般的な歯車の歯形を計算して描画できるようになりました。

スライダーを動かすこといろいろな歯車を描くことができます。

LANG: p5js_live
const sk = sketch;
tools = [];

// スライダーコントロールを作成するユーティリティ
// ラベルと値表示を追加する
function myCreateSlider(label, i, option) {
  let x = 20,
    y = i * 30 + 20;
  // ラベル
  const span = sk.createSpan(label).position(x, y);
  // 値表示
  const value = sk.createSpan(option[2]);
  value.position(x + 370, y);
  // スライダー
  const slider = sk.createSlider(...option);
  slider.position(x + 60, y);
  slider.size(300); // 値が変更されれば表示を更新
  slider.input(() => value.html(slider.value()));
  // ツールリストに登録
  tools.push(span, value, slider);
  // スライダーを返す
  return slider;
}

// チェックボックスを作成するユーティリティ
// ラベルをクリックしてもチェックできる
function myCreateCheckbox(label, i, j = 0) {
  let x = j * 100 + 20,
    y = i * 30 + 20;
  // チェックボックス
  const checkbox = sk.createCheckbox();
  checkbox.position(x, y);
  // ラベル (クリックでチェックを変更)
  const span = sk.createSpan(label).position(x + 20, y);
  span.mouseClicked(() => {
    checkbox.checked(!checkbox.checked());
    if (checkbox.elt.onchange) {
      checkbox.elt.onchange(); // 自動で呼ばれない
    }
  });
  span.style("cursor", "pointer ");
  // ツールリストに登録
  tools.push(span, checkbox);
  // チェックボックスを返す
  return checkbox;
}

let d = 0; // 時刻的な変数
const wsize = 640; // ウィンドウサイズ

// 初期化処理
sk.setup = () => {
  sk.createCanvas(wsize, wsize);
  checkHide = myCreateCheckbox("ツールを非表示", 19.5, 0);
  sliderM = myCreateSlider("スケール", 0, [10, 200, 40, 1]);
  sliderZA = myCreateSlider("歯数 A", 1, [6, 100, 8, 1]);
  sliderZB = myCreateSlider("歯数 B", 2, [6, 100, 15, 1]);
  sliderA = myCreateSlider("圧力角", 3, [10, 30, 20, 1]);
  sliderS = myCreateSlider("回転速度", 4, [-10, 50, 20, 1]);
  checkDk = myCreateCheckbox("歯先円", 5, 0);
  checkDp = myCreateCheckbox("基準円", 5, 1);
  checkDb = myCreateCheckbox("基礎円", 5, 2);
  checkDf = myCreateCheckbox("歯底円", 5, 3);
  tools.forEach((tool, i) => {
    tool.style("opacity", "0.2");
    tool.mouseOver(() => tools.forEach((t) => t.style("opacity", "1")));
    tool.mouseOut(() => tools.forEach((t) => t.style("opacity", "0.2")));
  });
  checkHide.elt.onchange = () => {
    tools.forEach((tool, i) => {
      if (i < 2) return; // 自分を消さない
      if (checkHide.checked()) {
        tool.hide();
      } else {
        tool.show();
      }
    });
  };
};

// 描画
sk.draw = () => {
  sk.background(220);
  sk.stroke("#00c");
  sk.strokeWeight(1);
  sk.noFill();

  const m = sliderM.value();
  const za = sliderZA.value();
  const zb = sliderZB.value();
  const alpha = (sliderA.value() / 360) * 2 * Math.PI;
  let gap = 0; // モジュールをほんの少し小さくすると空隙が生じるはず
  let dd = zb % 2 ? Math.PI / zb : 0; // 歯数の偶奇で位相を調整する
  draw_gear(
    wsize / 2 - (m * za) / 2,
    wsize / 2,
    d / za,
    m - gap / za,
    za,
    alpha
  );
  draw_gear(
    wsize / 2 + (m * zb) / 2,
    wsize / 2,
    -d / zb + dd,
    m - gap / zb,
    zb,
    alpha
  );

  d += 0.001 * sliderS.value();
};

// 歯車を描く(d は回転角度)
function draw_gear(ox, oy, d, m, z, a = (20 / 360) * 2 * Math.PI) {
  // 4 つの直径
  let rp = (m * z) / 2;
  let rk = rp + (2.0 * m) / 2;
  let rf = rp - (2.5 * m) / 2;
  let rb = rp * Math.cos(a);

  sk.drawingContext.setLineDash([5, 2, 5]);
  if (checkDp.checked()) sk.circle(ox, oy, rp * 2);
  if (checkDk.checked()) sk.circle(ox, oy, rk * 2);
  if (checkDf.checked()) sk.circle(ox, oy, rf * 2);
  if (checkDb.checked()) sk.circle(ox, oy, rb * 2);
  sk.drawingContext.setLineDash([1, 1]);

  // r からインボリュート角 a を求める関数
  const inva = (r, offset = 0) => {
    return Math.tan(Math.acos(rb / r)) - Math.acos(rb / r) - offset;
  };
  const a0 = inva(rp);

  let p1 = [];
  let n = 30,
    r0 = Math.max(rb, rf);
  // インボリュート曲線
  for (let j = 0; j <= n; j++) {
    let rj = r0 + ((rk - r0) * j) / n;
    p1.unshift([rj, inva(rj, a0)]);
  }
  // 垂直に下ろす
  if (rf < rb) {
    for (let j = 0; j <= n; j++) {
      let rj = r0 + ((rf - r0) * j) / n;
      p1.push([rj, inva(rb, a0)]);
    }
  }

  // 基準円が歯底円より大きいとき
  // 歯に逃げを追加するかどうか検討する
  let p = [];
  if (rb > rf) {
    // トロコイド曲線
    const trochoid = (dp, offset) => {
      let y = (Math.PI * m) / 4 - m * Math.tan(a) + rp * dp;
      let x = rp - m;
      let r = Math.sqrt(x * x + y * y);
      let theta = Math.atan2(y, x) - dp;
      return [r, theta + offset];
    };
    for (let dp = 0; p.length == 0 || p[0][0] < rp; dp += 0.01) {
      p.unshift(trochoid(dp, -Math.PI / z / 2));
    }

    // 合成する
    let j = 0;
    f = 0;
    for (let i = 0; i < p1.length - 1; i++) {
      if (p[j][0] < p1[i][0]) continue;
      while (j < p.length - 1 && p[j + 1][0] > p1[i][0]) j++;
      if (j >= p.length - 1) break;
      let pa =
        ((p[j][0] - p1[i][0]) * p[j + 1][1] +
          (p1[i][0] - p[j + 1][0]) * p[j][1]) /
        (p[j][0] - p[j + 1][0]);
      if (p1[i][1] < pa) f = 1;
      if (f && p1[i][1] >= pa) {
        p = p.slice(j - 1, -1);
        break;
      }
      p1[i][1] = Math.max(p1[i][1], pa);
    }
  }

  // 歯先の中心
  p1.unshift([p1[0][0], (Math.PI / z) * (1 / 2)]);
  // 歯底のフィレット半径
  if (true) {
    for (let i = 0; i < p1.length; i++) {
      const rr = m / 3; // フィレット半径
      if (p1[i][0] < rf + rr) {
        p1[i][1] +=
          -(rr / rf) *
          (1 - Math.sqrt(Math.max(0, 1 - ((rf + rr - p1[i][0]) / rr) ** 2)));
      }
    }
  }
  // 歯底の中心
  p1.push([rf, ((-Math.PI / z) * 1) / 2]);

  // 表示する
  for (let j = 0; j < z; j++) {
    let dd = ((2 * Math.PI) / z) * j;
    let dd2 = ((2 * Math.PI) / z) * (1 / 2 + j);
    for (let i = 0; i < p1.length - 1; i++) {
      sk.line(
        ox + p1[i][0] * Math.cos(d + p1[i][1] + dd),
        oy + p1[i][0] * Math.sin(d + p1[i][1] + dd),
        ox + p1[i + 1][0] * Math.cos(d + p1[i + 1][1] + dd),
        oy + p1[i + 1][0] * Math.sin(d + p1[i + 1][1] + dd)
      );
      sk.line(
        ox + p1[i][0] * Math.cos(d - p1[i][1] + dd2),
        oy + p1[i][0] * Math.sin(d - p1[i][1] + dd2),
        ox + p1[i + 1][0] * Math.cos(d - p1[i + 1][1] + dd2),
        oy + p1[i + 1][0] * Math.sin(d - p1[i + 1][1] + dd2)
      );
    }
  }
}

歯車の形状

参考: https://qiita.com/chromia/items/629311346c80dfd0eac7

gear.png

形状を指定するパラメータ

歯車は歯のピッチと歯の枚数、そして、歯の肉厚によって形が決まる。

  • モジュール (歯のピッチを $\pi$ で割った値) $m$
  • 歯の数 $z$
  • 圧力角(歯の肉厚を決める) $\alpha$

歯のピッチは円周の $1/z$ だから歯車の直径 $d$ として $\pi d/z$

ここから $mz=d$ の関係が出る

圧力角

  • 圧力角が小さいと歯と歯の滑りが小さくなり摩擦が小さくなるが、
    歯車が小さいときに接触時間が短くなる。
  • 圧力角が大きいと歯と歯の滑りが大きくなり摩擦が大きくなるが、
    歯車が小さいときも接触時間は長く取れる。

圧力角は 20°とするのが一般的だそう。

基準円とその他の円

上の $d$ は「基準円」の直径であり、他の直径と区別するため以下では $d_p$ と書く。

  • かみ合う2つの歯車は基準円同士がちょうど接する
  • 基準円の上で歯の厚さはピッチの半分になり、残りの半分が隙間になる
  • 基準円の上で歯の外形線と半径とがなす角が圧力角 $\alpha$ である
gear2.png

その他に重要な直径として以下がある。

  • 歯先円(歯の先端) $d_k=d_p+2m$
  • 基礎円(歯の外形と半径とが接する) $d_b=d_p\cos \alpha$
  • 歯底円(歯間の底) $d_f=d_p-2.5m$

基礎円の外側の歯形

基礎円より外側の歯の形状はインボリュート曲線とすることが多いらしい。 この曲線は外形線が基礎円と交わる点を基準とした極座標 $r,\theta$ を用いて、 $$ \theta=\tan a-a\ \ \text{ただし}\ \ a(r)=\cos^{-1}\frac{r_b}r $$ と表される。ここに現れる $$ \mathrm{inv}\,a=\tan a-a $$ はインボリュート関数と呼ばれる。

gear3.png

基準円の内側の歯形

特にこだわらないのであれば基礎円より下はまっすぐ中心へ下ろせばよいのであるが、より丈夫にする為に歯の根元にフィレットを付けることも多い。フィレットは直径 $d_f+m$ あたりから緩やかに歯底円へ落とす。

歯数の少ない歯車では

歯数が少ない歯車、具体的には歯数が 18 未満になると、相手の歯数が大きいときに基準円より下で相手の歯先と干渉してしまう。

相手の歯数が大きいほど干渉は大きくなるため、相手の歯数が無限大でも大丈夫な形で「逃げ」を作ろう。歯数無限大の歯車はラックであるから、以下ではラック&ピニオンでの状況を考える。ラックの歯の角度は圧力角 $\alpha$ そのもので、$r_p$ に相当する高さではやはり歯の幅はピッチの半分になる。高さは基準円から上が $m$、下が $1.25m$ である。

gear4.png

ラックの歯先が通る直線はピニオンの中心から $r_p-m$ の距離にあり、ラックの移動速度はピニオンの半径 $r_p$ の点の円周の速さと同じである。

下図の時点で頂点の $y$ 座標は $\pi m/4-m \tan \alpha$ である。このあとピニオンが $\omega t$ 回転したとき $y$ 座標は $\pi m/4-m \tan \alpha-r_p\omega t$ で $x$ 座標は $x=r_p-m$ で変わらないから、この点をピニオンの中心から見た極座標で表した曲座標 $\theta$ と動径 $r$ は

$$ r=\sqrt{(r_p-m)^2+(\pi m/4-m \tan \alpha-r_p\omega t)^2} $$ $$ \theta=\text{atan}\,\frac{\pi m/4-m \tan \alpha-r_p\omega t}{r_p-m} $$ このときピニオン自体 $\omega t$ 回転しているのでピニオンと共に回転する座標では $$ \theta'=\theta-\omega t $$ となる。この曲線はトロコイド曲線と呼ばれるそうだ。

これがインボルート曲線より内側に入る部分を取り除いて、適当なフィレットを付けて求めたのが冒頭の歯車の形状である。

歯形曲線生成

LANG: p5js_live
sk = sketch;
tools = [];

// スライダーコントロールを作成するユーティリティ
// ラベルと値表示を追加する
function myCreateSlider(label, i, option) {
  let x = 20,
    y = i * 30 + 20;
  // ラベル
  const span = sk.createSpan(label).position(x, y);
  // 値表示
  const value = sk.createSpan(option[2]);
  value.position(x + 370, y);
  // スライダー
  const slider = sk.createSlider(...option);
  slider.position(x + 60, y);
  slider.size(300); // 値が変更されれば表示を更新
  slider.input(() => value.html(slider.value()));
  // ツールリストに登録
  tools.push(span, value, slider);
  // スライダーを返す
  return slider;
}

// チェックボックスを作成するユーティリティ
// ラベルをクリックしてもチェックできる
function myCreateCheckbox(label, i, j = 0, checked = false) {
  let x = j * 100 + 20,
    y = i * 30 + 20;
  // チェックボックス
  const checkbox = sk.createCheckbox();
  checkbox.position(x, y);
  checkbox.checked(checked);
  // ラベル (クリックでチェックを変更)
  const span = sk.createSpan(label).position(x + 20, y);
  span.mouseClicked(() => {
    checkbox.checked(!checkbox.checked());
    if (checkbox.elt.onchange) {
      checkbox.elt.onchange(); // 自動で呼ばれない
    }
  });
  span.style("cursor", "pointer ");
  // ツールリストに登録
  tools.push(span, checkbox);
  // チェックボックスを返す
  return checkbox;
}

let d = 0; // 時刻的な変数
const wsize = 640; // ウィンドウサイズ

// 初期化処理
sk.setup = () => {
  sk.createCanvas(wsize, wsize);
  checkHide = myCreateCheckbox("ツールを非表示", 19.5, 0);
  sliderM = myCreateSlider("スケール", 0, [10, 400, 150, 1]);
  sliderZ = myCreateSlider("歯数", 1, [4, 100, 8, 1]);
  sliderA = myCreateSlider("圧力角", 2, [10, 30, 20, 1]);
  checkDk = myCreateCheckbox("歯先円", 3, 0, true);
  checkDp = myCreateCheckbox("基準円", 3, 1, true);
  checkDb = myCreateCheckbox("基礎円", 3, 2, true);
  checkDf = myCreateCheckbox("歯底円", 3, 3, true);
  tools.forEach((tool, i) => {
    tool.style("opacity", "0.2");
    tool.mouseOver(() => tools.forEach((t) => t.style("opacity", "1")));
    tool.mouseOut(() => tools.forEach((t) => t.style("opacity", "0.2")));
  });
  checkHide.elt.onchange = () => {
    tools.forEach((tool, i) => {
      if (i < 2) return; // 自分を消さない
      if (checkHide.checked()) {
        tool.hide();
      } else {
        tool.show();
      }
    });
  };
}

// 描画
sk.draw = () => {
  sk.background(220);
  sk.strokeWeight(1);
  sk.noFill();

  const m = sliderM.value();
  const z = sliderZ.value();
  const alpha = (sliderA.value() / 360) * 2 * Math.PI;

  let gap = 0; // モジュールをほんの少し小さくすると空隙が生じるはず

  draw_gear(wsize / 2, wsize / 2 + (m * z) / 2, d / z, m - gap / z, z, alpha);
}

// 歯車を描く(d は回転角度)
function draw_gear(ox, oy, d, m, z, a = (20 / 360) * 2 * Math.PI) {
  // 4 つの直径
  let rp = (m * z) / 2;
  let rk = rp + (2.0 * m) / 2;
  let rf = rp - (2.5 * m) / 2;
  let rb = rp * Math.cos(a);

  sk.drawingContext.setLineDash([5, 2, 5]);
  if (checkDp.checked()) sk.circle(ox, oy, rp * 2);
  if (checkDk.checked()) sk.circle(ox, oy, rk * 2);
  if (checkDf.checked()) sk.circle(ox, oy, rf * 2);
  if (checkDb.checked()) sk.circle(ox, oy, rb * 2);
  sk.drawingContext.setLineDash([1, 1]);

  function drawCurve(p) {
    const d = (-2 * Math.PI) / z / 4 - Math.PI / 2;
    let x1, y1, x2, y2;
    for (let i = 0; i < p.length; i++) {
      x2 = x1;
      y2 = y1;
      x1 = p[i][0] * Math.cos(+p[i][1] + d);
      y1 = p[i][0] * Math.sin(+p[i][1] + d);
      sk.circle(ox + x1, oy + y1, 6);
      sk.circle(ox - x1, oy + y1, 6);
      if (i == 0) continue;
      sk.line(ox + x2, oy + y2, ox + x1, oy + y1);
      sk.line(ox - x2, oy + y2, ox - x1, oy + y1);
      sk.circle(ox + x2, oy + y2, 6);
      sk.circle(ox - x2, oy + y2, 6);
    }
  }

  // r からインボリュート角 a を求める関数
  const a0 = Math.tan(Math.acos(rb / rp)) - Math.acos(rb / rp);
  const inva = (r) => {
    return Math.tan(Math.acos(rb / r)) - Math.acos(rb / r) - a0;
  };

  // 歯先から順に書く
  const curve = [];
  const n = 15;
  const r0 = Math.max(rb, rf);
  for (let i = 0; i <= n; i++) {
    let r = rk + ((r0 - rk) * i) / n;
    curve.push([r, inva(r, a0)]);
  }
  const fr = m / 4;
  if (r0 != rf) {
    curve.push([rf, inva(r0, a0)]);
  }
  if (curve[0][1] > Math.PI / z / 2) {
    let i;
    for (i = 0; curve[i + 1][1] > Math.PI / z / 2; i++) {}
    const p = [
      -(curve[i][0] * (curve[i + 1][1] - Math.PI / z / 2) +
        curve[i + 1][0] * (Math.PI / z / 2 - curve[i][1])) /
        (curve[i][1] - curve[i + 1][1]),
      Math.PI / z / 2,
    ];
    curve.splice(0, i+1);
    curve.unshift(p);
  } else {
    curve.unshift([rk, Math.PI / z / 2]); // 歯先の中心
  }

  // drawCurve(curve);

  // トロコイド曲線
  const trochoid = (dp) => {
    let y = (Math.PI * m) / 4 - m * Math.tan(a) + rp * dp;
    let x = rp - m;
    let r = Math.sqrt(x * x + y * y);
    let theta = Math.atan2(y, x) - dp;
    return [r, theta - Math.PI / z / 2];
  };

  const undercut = [];
  const dx = 0.15 / Math.sqrt(z);
  for (let x = 0; x < 1; x += dx) {
    undercut.unshift(trochoid(x));
    if (undercut.length < 2) continue;
    if (undercut[0][0] < rp) continue;
    if (undercut[0][0] > rk) break;
    if (inva(r0) > undercut[0][1] && undercut[0][1] < undercut[1][1]) break;
  }
  for (
    let x = 0;
    x > -1 && undercut[undercut.length - 1][1] > inva(r0);
    x -= dx
  ) {
    undercut.push(trochoid(x));
    if (undercut[undercut.length - 1][0] > undercut[undercut.length - 2][0])
      break;
  }
  // stroke("#00c");
  // drawCurve(undercut);

  const result = [];
  let flag = true;
  for (let i = 0, j = 0; ; ) {
    if (j >= undercut.length) {
      result.push(...curve.slice(i));
      break;
    }

    const ci = curve[i];
    const ci1 = i < curve.length - 1 ? curve[i + 1] : ci;
    const uj = undercut[j];
    const uj1 = j < undercut.length - 1 ? undercut[j + 1] : uj;

    // 交差するか?
    // https://qiita.com/zu_rin/items/09876d2c7ec12974bc0f
    const s =
      ((uj[0] - ci[0]) * (uj1[1] - uj[1]) -
        (uj[1] - ci[1]) * (uj1[0] - uj[0])) /
      ((ci1[0] - ci[0]) * (uj1[1] - uj[1]) -
        (ci1[1] - ci[1]) * (uj1[0] - uj[0]));
    const t =
      ((ci[0] - uj[0]) * (ci1[1] - ci[1]) -
        (ci[1] - uj[1]) * (ci1[0] - ci[0])) /
      ((uj1[0] - uj[0]) * (ci1[1] - ci[1]) -
        (uj1[1] - uj[1]) * (ci1[0] - ci[0]));
    const cross = [ci[0] + (ci1[0] - ci[0]) * s, ci[1] + (ci1[1] - ci[1]) * s];
    const crossing = 0 <= s && s <= 1 && 0 <= t && t <= 1;

    // 半径の大きい方から比較する
    // 位相は大きい方を採用する

    if (crossing) {
      if (flag) {
        result.push(ci);
      } else {
        result.push(uj);
      }
      result.push(cross);
      if (ci[0] > uj[0]) {
        i++;
      } else {
        j++;
      }
      flag = ci1[1] > uj1[1];
    } else {
      if (ci1[0] < uj1[0]) {
        if (!flag) result.push(uj);
        j++;
      } else {
        if (flag) result.push(ci);
        i++;
      }
    }
  }
  
  // フィレット処理
  if(true) {
    const fr = m/3;
    const r2drda = (r) => {
      let cos_t = 1 - (r-rf)/fr;
      if(cos_t < 0) cos_t = 0;
      return r * Math.sqrt(1-cos_t^2) / cos_t;
    }
    let i;
    let drda, drda2;
    for(i=result.length-1; ; i--) {
      if(result[i][0] >= rf + fr) break;
      drda = (result[i-1][0] - result[i][0])/(result[i-1][1] - result[i][1]);
      drda2 = r2drda((result[i-1][0] + result[i][0])/2);
      if(drda2 > drda) {
        break;
      }
    }
    let cos_t = 1 - (result[i][0]-rf)/fr;
    if(cos_t < 0) cos_t = 0;
    let t = Math.acos(cos_t);
    const a1 = result[i][1] - fr * Math.sin(t) / (rf + fr);
    let n = Math.ceil(t/0.3);
    let dt = t / n;
    result.splice(i);
    for(let i = 0; i <= n; i++, t-=dt) {
      const r = rf+fr*(1-Math.cos(t));
      const aa = fr*Math.sin(t)/r + a1;
      if(aa < -Math.PI / z / 2) break;
      result.push([r, aa]);
    }
    result.push([result[result.length-1][0], -Math.PI / z / 2]);
  }
  
  drawCurve(result);
}

コメント・質問





Counter: 155 (from 2010/06/03), today: 1, yesterday: 13