歯車について勉強する6 の履歴(No.5)
更新歯車について勉強するシリーズ†
- 工作/歯車について勉強する 歯車の形状についての基礎
- 工作/歯車について勉強する2 仮想的なラックを使って実用的な歯車形状を切り出す話
- 工作/歯車について勉強する3 歯車に関するいろいろな計算機
- 工作/歯車について勉強する4 転位歯車の中心間距離と逆インボリュート関数
- 工作/歯車について勉強する5 ウォームホイールの歯形計算について
- 工作/歯車について勉強する6 かさ歯車は球面インボリュート曲線を持つ
- 工作/歯車について勉強する/ゼネバ歯車 間欠歯車の一種です
- 工作/Fusion360歯車スクリプト 勉強した結果を使って作ってみました
- 工作/Fusion360歯車切削スクリプト Fusion 360 内で歯車の歯切りを行うスクリプトです
- 工作/Fusion360曲面生成スクリプト 歯切り結果の表面をきれいにするスクリプトです
- 工作/Fusion360ジョイント駆動スクリプト ジョイントを動かしてアニメーションさせるスクリプトです
かさ歯車の歯形について†
かさ歯車の歯形は球面上に描くことになるため、平面上での歯形計算とはずいぶん異なる計算方法が必要になることが分かりました。
描けるようになりました†
画面のドラッグでカメラ位置を回転できます。 マウスホイールでズームできます。
LANG: p5js_live
(() => {
// src/css-text.ts
function generateRandomId(length) {
let result = "";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
let counter = 0;
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result;
}
var cssText = (id) => `
@media screen and (min-width: 640px) {
#${id} {
position: absolute;
left: 0;
top: 0;
opacity: 0.2;
}
#${id}:hover {
opacity: 0.7;
}
#${id} .control-slider {
padding-left: 10px;
}
}
@media screen and (max-width: 639px) {
#${id} .check-hide-tools {
display: none !important;
}
#${id} {
height: 30vh;
overflow-y: scroll;
line-height: 2;
}
#${id} > :last-child {
padding-bottom: 10vh;
}
#${id} .control-checkbox {
width:85px !important;
}
#${id} .control-slider input {
width: 200px !important;
}
}
#${id} {
display: block;
max-width: 500px
}
#${id} .control-slider {
display: block
}
#${id} .control-slider :first-child {
display: inline-block;
width: 80px
}
#${id} .control-slider :nth-child(2) {
display: inline-block;
width: 50px
}
#${id} .control-slider input {
display: inline-block;
width: 350px;
}
#${id} .control-checkbox {
display:inline-block;
width:110px;
overflow:hidden
}
`;
// src/initializeControls.ts
function initializeControls(parent, inputChanged = () => {
}) {
const id = generateRandomId(8);
const wrapper = elem("div", "", [elem("style", cssText(id))], { id });
parent.appendChild(wrapper);
const wrapControls = elem("div", "", [], {});
const checkHide = createCheckbox(wrapper, "ツールを非表示", () => {
wrapControls.style.display = checkHide.checked ? "none" : "block";
});
checkHide.parentElement.classList.add("check-hide-tools");
wrapper.appendChild(wrapControls);
const controls = {
alpha: createSlider(
wrapControls,
"圧力角",
{ min: 10, max: 32, value: 20, step: 1 },
inputChanged
),
z1: createSlider(wrapControls, "歯数1", { min: 4, max: 200, value: 12, step: 1 }, inputChanged),
z2: createSlider(wrapControls, "歯数2", { min: 4, max: 200, value: 30, step: 1 }, inputChanged),
axesAngle: createSlider(
wrapControls,
"軸角度",
{ min: 20, max: 160, value: 90, step: 5 },
inputChanged
),
backlash: createSlider(
wrapControls,
"バックラッシュ",
{ min: -1, max: 1, value: 0, step: 0.02 },
inputChanged
),
theta: createSlider(
wrapControls,
"θ",
{ min: -5, max: 5, value: 0, step: 5e-3 },
inputChanged
),
stop: false
};
controls.theta.addEventListener("mousedown", () => {
controls.stop = true;
});
controls.theta.addEventListener("mouseup", () => {
controls.stop = false;
});
return {
get z1() {
return Number(controls.z1.value);
},
get z2() {
return Number(controls.z2.value);
},
get alpha() {
return Number(controls.alpha.value) / 180 * Math.PI;
},
get fillet() {
return 0.4;
},
get mk() {
return 1;
},
get mf() {
return 1.25;
},
get axesAngle() {
return Number(controls.axesAngle.value);
},
get theta() {
return Number(controls.theta.value);
},
get backlash() {
return Number(controls.backlash.value);
},
set theta(value) {
controls.theta.value = value.toPrecision(4);
controls.theta.doInput();
},
incrementTheta() {
const delta = 0.01;
if (controls.stop) return;
if (this.theta + delta >= Number(controls.theta.max)) {
this.theta = this.theta + delta - Number(controls.theta.max) + Number(controls.theta.min);
} else {
this.theta += delta;
}
}
};
}
function elem(tag, html = "", children = [], props = {}, events = {}) {
const result = document.createElement(tag);
if (html != "") result.innerHTML = html;
children.forEach((c) => result.appendChild(c));
Object.entries(props).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0).forEach(([k, v]) => {
if (k == "class") {
result.className = String(v);
} else if (typeof v == "object") {
Object.entries(v).forEach(([k2, v2]) => result[k][k2] = v2);
} else {
result[k] = v;
}
});
const notIsArray = (maybeArray) => {
return !Array.isArray(maybeArray);
};
Object.entries(events).forEach(([k, v]) => {
if (notIsArray(v)) v = [v];
v.forEach((f) => result.addEventListener(k, f));
});
return result;
}
function createSlider(parent, label, option, oninput) {
const value = elem("span", String(option.value));
const slider = elem(
"input",
"",
[],
{ type: "range", ...option },
{ input: () => slider.doInput() }
);
slider.doInput = () => {
value.innerHTML = slider.value;
oninput();
};
const wrapper = elem("div", "", [elem("span", label), value, slider], {
class: "control-slider"
});
parent.appendChild(wrapper);
return slider;
}
function createCheckbox(parent, label, oninput, checked = false) {
const check = elem(
"input",
"",
[],
{ type: "checkbox", checked },
{ input: (e) => check.doInput(e) }
);
check.doInput = oninput;
const wrapper = elem("label", "", [check, elem("span", label)], {
class: "control-checkbox"
});
parent.appendChild(wrapper);
return check;
}
// src/function.ts
function minimize(l, r, f, epsilon = 1e-6) {
const phi = (1 + Math.sqrt(5)) / 2;
let m1 = r - (r - l) / phi;
let m2 = l + (r - l) / phi;
let f1 = f(m1);
let f2 = f(m2);
while (Math.abs(l - r) > epsilon) {
if (f1 < f2) {
r = m2;
m2 = m1;
f2 = f1;
m1 = r - (r - l) / phi;
f1 = f(m1);
} else {
l = m1;
m1 = m2;
f1 = f2;
m2 = l + (r - l) / phi;
f2 = f(m2);
}
}
return (l + r) / 2;
}
// src/vector3d.ts
var Vector3D = class _Vector3D {
x;
y;
z;
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
static polar(r, theta, phi) {
return new _Vector3D(
r * Math.sin(phi) * Math.cos(theta),
r * Math.sin(phi) * Math.sin(theta),
r * Math.cos(phi)
);
}
angle() {
return Math.atan2(this.y, this.x);
}
equal(v) {
const epsilon = 1e-6;
return Math.abs(this.x - v.x) < epsilon && Math.abs(this.y - v.y) < epsilon && Math.abs(this.z - v.z) < epsilon;
}
add(v) {
return new _Vector3D(this.x + v.x, this.y + v.y, this.z + v.z);
}
sub(v) {
return new _Vector3D(this.x - v.x, this.y - v.y, this.z - v.z);
}
mul(scalar) {
return new _Vector3D(this.x * scalar, this.y * scalar, this.z * scalar);
}
rotate(axis, angle) {
axis = axis.normalize();
const cos2 = Math.cos(angle);
const sin2 = Math.sin(angle);
const { x, y, z } = this;
const { x: ux, y: uy, z: uz } = axis;
const dot = this.inner(axis);
const cross = this.outer(axis);
return new _Vector3D(
x * cos2 + cross.x * sin2 + ux * dot * (1 - cos2),
y * cos2 + cross.y * sin2 + uy * dot * (1 - cos2),
z * cos2 + cross.z * sin2 + uz * dot * (1 - cos2)
);
}
flipY() {
return new _Vector3D(this.x, -this.y, this.z);
}
flipX() {
return new _Vector3D(-this.x, this.y, this.z);
}
flipZ() {
return new _Vector3D(this.x, this.y, -this.z);
}
norm() {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
normalize(length = 1) {
const len = this.norm() / length;
return new _Vector3D(this.x / len, this.y / len, this.z / len);
}
polar() {
return [this.norm(), this.angle()];
}
inner(v) {
return this.x * v.x + this.y * v.y + this.z * v.z;
}
outer(v) {
return new _Vector3D(
this.y * v.z - this.z * v.y,
this.z * v.x - this.x * v.z,
this.x * v.y - this.y * v.x
);
}
cross(v) {
return this.outer(v);
}
distance(v) {
return v.sub(this).norm();
}
normalVectors() {
const x = this.x;
const y = this.y;
const z = this.z;
if (Math.abs(x) > Math.abs(y)) {
return [new _Vector3D(-z, 0, x).normalize(), new _Vector3D(0, -z, y).normalize()];
} else {
return [new _Vector3D(0, -z, y).normalize(), new _Vector3D(-z, 0, x).normalize()];
}
}
};
// src/main.ts
var { abs, sqrt, max, sin, cos, tan, asin, acos, atan, atan2, PI } = Math;
var cameraP = new Vector3D(3, 0, 0);
var cameraU = new Vector3D(0, -1, 0);
var cameraV = new Vector3D(0, 0, 1);
var cameraW = PI / 8;
var sketch = (p2) => {
let params;
const canvas = {
width: 800,
height: 800,
ox: 0,
oy: 0
};
p2.mouseDragged = (e) => {
if (e.target.tagName !== "CANVAS") return;
cameraP = cameraP.sub(cameraU.mul(2 * (p2.mouseX - p2.pmouseX) * (cameraP.norm() - 1) / canvas.width)).normalize(cameraP.norm());
cameraU = cameraP.outer(cameraV).normalize();
cameraP = cameraP.sub(cameraV.mul(2 * (p2.mouseY - p2.pmouseY) * (cameraP.norm() - 1) / canvas.width)).normalize(cameraP.norm());
cameraV = cameraP.outer(cameraU).normalize(-1);
inputChanged();
return true;
};
p2.mouseWheel = (event) => {
if (event.target.tagName !== "CANVAS") return;
const delta = event.deltaY / 200;
let n = cameraP.norm() * (1 + delta);
n = Math.max(1.2, n);
cameraP = cameraP.normalize(n);
inputChanged();
event.preventDefault();
return true;
};
p2.setup = () => {
const { width, height } = canvas;
canvas.ox = width / 2;
canvas.oy = height * 2 / 3;
const c = p2.createCanvas(width, height);
c.style("max-width", "100%");
c.style("height", "auto");
const wrapper = c.elt.parentElement;
params = initializeControls(wrapper, inputChanged);
inputChanged();
setInterval(() => {
params.incrementTheta();
}, 100);
};
const inputChanged = () => {
const { width, height } = canvas;
const sigma = params.axesAngle * PI / 180;
const gammaP1 = atan2(sin(sigma), params.z2 / params.z1 + cos(sigma));
const gammaP2 = sigma - gammaP1;
const r = params.z1 / (2 * sin(gammaP1));
const rm = 1 / r;
const axis1 = new Vector3D(cos(gammaP1), 0, sin(gammaP1));
const axis2 = new Vector3D(cos(gammaP2), 0, -sin(gammaP2));
const eps = 1e-5;
function proj(lambdaOrVec, phi = 0) {
if (typeof lambdaOrVec === "number") {
lambdaOrVec = new Vector3D(
cos(phi) * cos(lambdaOrVec),
sin(phi) * cos(lambdaOrVec),
sin(lambdaOrVec)
);
} else {
lambdaOrVec = lambdaOrVec.normalize();
}
const p3 = lambdaOrVec.sub(cameraP);
if (p3.inner(lambdaOrVec) > 0) return [NaN, NaN];
const u = p3.inner(cameraU);
const v = p3.inner(cameraV);
const d = -p3.inner(cameraP.normalize());
return [(1 + atan2(u, d) / cameraW) * (width / 2), (1 + atan2(v, d) / cameraW) * (width / 2)];
}
function isArrayOf(obj, predicate) {
return Array.isArray(obj) && obj.every(predicate);
}
function drawCurve(points) {
if (points.length === 0) return;
if (isArrayOf(points, (item) => item instanceof Vector3D)) {
points = points.map((p3) => proj(p3));
}
let last = [NaN, NaN];
points.forEach((point) => {
if (isNaN(point[0])) {
if (!isNaN(last[0])) {
p2.curveVertex(...last);
p2.endShape();
}
last = point;
return;
}
if (isNaN(last[0])) {
p2.beginShape();
p2.curveVertex(...point);
}
p2.curveVertex(...point);
last = point;
});
if (!isNaN(last[0])) {
p2.curveVertex(...last);
p2.endShape();
}
}
const initializeCanvas = () => {
p2.fill(220);
p2.rect(0, 0, width, height);
p2.stroke(100);
for (let j = 0; j <= 18; j++) {
const phi = j * PI / 18 - PI / 2;
const points = [];
for (let lambda = -PI; lambda <= PI; lambda += PI / 60 - eps) {
points.push(proj(phi, lambda));
}
if (j % 9 === 0) {
p2.stroke(160);
} else {
p2.stroke(200);
}
p2.noFill();
drawCurve(points);
}
for (let i = 0; i <= 36; i++) {
const lambda = i * PI / 18;
const points = [];
for (let phi = -PI / 2; phi <= PI / 2; phi += PI / 30 - eps) {
const pp = proj(phi, lambda);
points.push(pp);
}
if (i % 9 === 0) {
p2.stroke(160);
} else {
p2.stroke(200);
}
p2.noFill();
drawCurve(points);
}
(() => {
p2.drawingContext.setLineDash([10, 10]);
const points = [];
const a = new Vector3D(0, cos(params.alpha), -sin(params.alpha));
const v = new Vector3D(1, 0, 0);
for (let i = 0; i <= 20; i++) {
points.push(proj(v.rotate(a, (-0.1 + 0.2 * i / 20) * PI)));
}
drawCurve(points);
p2.drawingContext.setLineDash([]);
})();
(() => {
p2.drawingContext.setLineDash([10, 10]);
const points = [];
const a = new Vector3D(0, sin(params.alpha), cos(params.alpha));
const v = new Vector3D(1, 0, 0);
for (let i = 0; i <= 20; i++) {
points.push(proj(v.rotate(a, (-0.1 + 0.2 * i / 20) * PI)));
}
drawCurve(points);
p2.drawingContext.setLineDash([]);
})();
};
initializeCanvas();
function drawCircle(axis, delta) {
const [axis1u, axis1v] = axis.normalVectors();
const points = [];
const n = 36;
for (let i = 0; i <= 2 * n; i++) {
const t = i * PI / n;
const p1 = axis.add(axis1u.mul(tan(delta) * cos(t))).add(axis1v.mul(tan(delta) * sin(t)));
points.push(proj(p1));
}
drawCurve(points);
}
p2.stroke(0);
p2.strokeWeight(2);
p2.circle(...proj(axis1), 2);
p2.circle(...proj(axis2), 2);
const gammaF1 = gammaP1 - params.mf * atan(rm);
const gammaK1 = gammaP1 + params.mk * atan(rm);
const gammaT1 = gammaP1 + params.mf * atan(rm);
const gammaF2 = gammaP2 - params.mf * atan(rm);
const gammaK2 = gammaP2 + params.mk * atan(rm);
const gammaT2 = gammaP2 + params.mf * atan(rm);
p2.stroke(160);
p2.strokeWeight(1);
p2.noFill();
p2.drawingContext.setLineDash([10, 10]);
drawCircle(axis1, gammaP1);
drawCircle(axis1, gammaF1);
drawCircle(axis1, gammaK1);
drawCircle(axis1, gammaT1);
drawCircle(axis2, gammaP2);
drawCircle(axis2, gammaF2);
drawCircle(axis2, gammaK2);
drawCircle(axis2, gammaT2);
p2.drawingContext.setLineDash([]);
const gammaB1 = asin(cos(params.alpha) * sin(gammaP1));
const gammaB2 = asin(cos(params.alpha) * sin(gammaP2));
drawCircle(axis1, gammaB1);
drawCircle(axis2, gammaB2);
function trans(epsilon, v, axis, gammaB) {
axis = axis.normalize(cos(gammaB));
v = v.normalize();
const oq = v.sub(axis);
const om = oq.rotate(axis, epsilon);
const oom = axis.add(om);
const axis22 = oom.sub(axis.normalize(1 / cos(gammaB)));
const op = oom.rotate(axis22, epsilon * sin(gammaB));
return op;
}
function sphericalInvolute(axis, v, gammaB, gammaF, gammaK) {
const points = [];
for (let i = 0; i <= 20; i++) {
const t = gamma2epsilon(gammaF, gammaB) + (gamma2epsilon(gammaK, gammaB) - gamma2epsilon(gammaF, gammaB)) * i / 20;
points.push(trans(t, v, axis, gammaB));
}
return points;
}
function gamma2theta(gamma, gammaB) {
const varphi = acos(cos(gamma) / cos(gammaB));
return varphi / sin(gammaB) - atan2(tan(varphi), sin(gammaB));
}
function gamma2epsilon(gamma, gammaB) {
const varphi = acos(cos(gamma) / cos(gammaB));
return varphi / sin(gammaB);
}
function drawArc(axis, p1, p2Angle, options = {}) {
const { greatCircle = false, n = 6 } = options;
const c = [];
if (greatCircle) {
const v1 = p1;
const v2 = typeof p2Angle === "number" ? v1.rotate(axis, p2Angle) : p2Angle;
const t = acos(v1.inner(v2) / (v1.norm() * v2.norm()));
for (let j = 0; j <= n; j++) {
const t22 = t * j / n;
c.push(v1.rotate(axis, t22));
}
} else {
const v1 = p1.sub(axis);
const v2 = typeof p2Angle === "number" ? v1.rotate(axis, p2Angle) : p2Angle.sub(axis);
const t = acos(v1.inner(v2) / (v1.norm() * v2.norm()));
for (let j = 0; j <= n; j++) {
const t22 = t * j / n;
c.push(axis.add(v1.rotate(axis, t22)));
}
}
drawCurve(c);
}
function rotateAround(v, axis, angle) {
axis = axis.normalize(axis.normalize().inner(v));
return axis.add(v.sub(axis).rotate(axis, angle));
}
p2.stroke(0);
const q1 = new Vector3D(cos(gammaP1 - gammaB1), 0, sin(gammaP1 - gammaB1)).rotate(
axis1,
-gamma2theta(gammaP1, gammaB1)
);
const tooth1 = sphericalInvolute(axis1, q1, gammaB1, max(gammaB1, gammaF1), gammaK1);
const t1 = trans(gamma2epsilon(gammaT1, gammaB1), q1, axis1, gammaB1);
const q2 = new Vector3D(cos(gammaP2 - gammaB2), 0, -sin(gammaP2 - gammaB2)).rotate(
axis2,
-gamma2theta(gammaP2, gammaB2)
);
const tooth2 = sphericalInvolute(axis2, q2, gammaB2, max(gammaB2, gammaF2), gammaK2);
const t2 = trans(gamma2epsilon(gammaT2, gammaB2), q2, axis2, gammaB2);
function trochoid(t12, q22, axis12, axis22, z1, z2, tooth22, gammaB22, gammaF22) {
const trochoid22 = (t) => rotateAround(rotateAround(t12, axis12, -t), axis22, -t / z2 * z1);
const c1 = [];
let t1a, t1b, t1c;
for (let i = 0; ; i++) {
const t = 0.02 * (i * PI) / sqrt(params.z1);
c1.push(trochoid22(t));
if (c1.at(-1).sub(axis22).norm() > tooth22.at(-1).sub(axis22).norm()) {
t1a = minimize(0, t, (t3) => trochoid22(t3).sub(axis22).norm());
t1b = t;
break;
}
}
if (gammaB22 > gammaF22) {
t1c = minimize(t1a, t1b, (t) => abs(trochoid22(t).sub(axis22).norm() - q22.sub(axis22).norm()));
} else {
t1c = t1a;
}
const t1d = minimize(t1c, t1b, (t) => {
const v = trochoid22(t);
const gamma3 = acos(v.inner(axis22));
const u = trans(gamma2epsilon(gamma3, gammaB22), q22, axis22, gammaB22);
return u.sub(v).norm();
});
c1.splice(0);
for (let i = 0; i <= 10; i++) {
const t = t1a + (t1d - t1a) * i / 10;
c1.push(trochoid22(t));
}
const gamma = acos(c1.at(-1).inner(axis22));
return [c1, gamma];
}
const [trochoid1, gamma1] = trochoid(
t2,
q1,
axis2,
axis1,
params.z2,
params.z1,
tooth1,
gammaB1,
gammaF1
);
const involute1 = sphericalInvolute(axis1, q1, gammaB1, gamma1, gammaK1);
drawGear(axis1, involute1, trochoid1, params.z1, params.theta);
const [trochoid2, gamma2] = trochoid(
t1,
q2,
axis1,
axis2,
params.z1,
params.z2,
tooth2,
gammaB2,
gammaF2
);
const involute2 = sphericalInvolute(axis2, q2, gammaB2, gamma2, gammaK2);
drawGear(axis2, involute2, trochoid2, params.z2, -params.theta);
return;
function drawGear(axis, involute, trochoid3, z, theta) {
function angle(p3) {
return acos(
p3.sub(axis.normalize(p3.inner(axis))).normalize().inner(new Vector3D(1, 0, 0).sub(axis.normalize(new Vector3D(1, 0, 0).inner(axis))).normalize())
);
}
function backlash(curve) {
curve = curve.map((p3) => p3.rotate(axis, params.backlash * PI / z / 10));
if (angle(curve[0]) > PI / z / 2) {
while (angle(curve[0]) > PI / z / 2)
curve.shift();
}
if (angle(curve.at(-1)) > PI / z / 2) {
while (angle(curve.at(-1)) > PI / z / 2)
curve.pop();
}
return curve;
}
involute = backlash(involute);
trochoid3 = backlash(trochoid3);
for (let i = 0; i < z; i++) {
const c1 = involute.map((p3) => p3.rotate(axis, i * 2 * PI / z)).map((p3) => p3.rotate(axis, 2 * PI / z * theta));
const c2 = involute.map((p3) => p3.flipY().rotate(axis, (i + 1 / 2) * 2 * PI / z)).map((p3) => p3.rotate(axis, 2 * PI / z * theta));
drawCurve(c1);
drawCurve(c2);
const c3 = trochoid3.map((p3) => p3.rotate(axis, i * 2 * PI / z)).map((p3) => p3.rotate(axis, 2 * PI / z * theta));
const c4 = trochoid3.map((p3) => p3.flipY().rotate(axis, (i + 1 / 2) * 2 * PI / z)).map((p3) => p3.rotate(axis, 2 * PI / z * theta));
drawCurve(c3);
drawCurve(c4);
drawArc(axis.normalize(axis.inner(c2.at(-1))), c1.at(-1), c2.at(-1));
drawArc(
axis.normalize(axis.inner(c3.at(0))),
c4.at(0),
c3.at(0).rotate(axis, 2 * PI / z)
);
}
}
};
};
if (true) {
sketch(p);
} else {
new p5(sketch, window["p5wrapper"]);
}
})();
球面インボリュート曲線†
通常の歯車の歯形にインボリュート曲線が用いられるのに対して、 かさ歯車の歯形には球面インボリュート曲線を用いると良いのだそうです。
球面インボリュート曲線は、図において基礎円の円弧 $\overset{\frown}{MQ}$ と大円の円弧 $\overset{\frown}{MP}$ との長さが等しくなるように取った曲線 $PQ$ のこと。
図において、球の半径を $r_0$ とすると、球の中心 $O_s$ から基礎円の半径 $r_b$ を見込む角度 $\gamma_b$ は $OsO$ と $OM$ とが直交するため $r_b=r_0\sin\gamma_b$ を満たす。
球の中心から $MP$ および $OP$ を見込む角度をそれぞれ $\varphi,\gamma$ とする。
このとき、 $$ \overset{\frown}{MQ}=\overset{\frown}{MP} $$ としたいので、大円の半径を $r_0$ とすれば、 $$ r_0\varepsilon\sin\gamma_b=r_0\varphi $$ が成り立つ必要があり、ここから、 $$ \varepsilon\sin\gamma_b=\varphi\tag{4} $$ を得る。
$\varphi(\gamma)$ を求めるには、 $$ \cos\gamma=\cos\varphi\cos\gamma_b\tag{6} $$ より得られる $$ \cos\varphi=\frac{\cos\gamma}{\cos\gamma_b} $$ により $$ \phi(\gamma)=\cos^{-1}\frac{\cos\gamma}{\cos\gamma_b} $$ とすればよい。 (4) と $$ \tan\varphi=\sin\gamma_b\tan\phi\tag{11} $$ を用いると、 $$ \begin{aligned} \theta(\gamma) &=\varepsilon(\gamma)-\phi(\gamma)\\ &=\frac{\varphi(\gamma)}{\sin\gamma_b}-\tan^{-1}\frac{\tan\varphi(\gamma)}{\sin\gamma_b}\\ \end{aligned} $$ として $\gamma$ から $\theta$ を求められる。
これが歯形そのものを表す関係式なのだと思うのだけれど、なぜかリンク先ではこの形の式が紹介されていないようだ???
実は上のような3D的な画を描くには、リンク先のように三角関数をごにょごにょするだけで計算しようとするととてもややこしい。上の式によって必要なパラメータを得た後は、点 $Q$ の座標を歯車の軸に対して $\varepsilon$ 左に回転して $M$ を見つけ、その後、対円に沿って $\varphi=\varepsilon\sin(\gamma_b)$ だけ回転することで点 $P$ を求められるので、そのようにして曲線を描く方が分かりやすいようだった。
歯元の形状†
本来なら通常の歯車と同様に相手の歯形の先端をフィレット付きで延長して歯元に自然なフィレットを付けられるよう計算したいところなのだけれど、 球面上でいい感じにフィレットを付けたり、フィレットの描く包絡線を計算するのが大変そうだったため、フィレットを付けずにそのまま歯先を延長し、その頂点が描く球面トロコイド曲線で歯元の形状を決定することにした。
下の歯形の左側のインボリュート曲線を頂隙分だけ延長した点を基準にして、 この点を軸1に対して $\theta/z_1$ だけ回して軸2に対して $\theta/z_2$ だけ戻す、 という操作により得られる点を $\theta$ を連続的に変化させながら結ぶことで球面トロコイド曲線が得られる。
図では上の歯の歯先円を超えるまでが、左図では基礎円が歯底円より小さい場合、右図では基礎円が歯底円より大きい場合、について描かれている。
この球面トロコイド曲線のうち、最も上の歯車の軸に近づくところから、基礎円より外で最もインボリュート曲線に近づくところまで、が歯形として利用される。
この計算では歯元が必要以上にえぐれてしまうため、相手歯先を正しく丸めた場合に比べると強度が低下してしまうものの、歯元形状をインボリュート曲線のままにしたり、基礎円から下を真下に下ろすのに比べれば多少なりともフィレットが付いている。また小径歯車に対して必要な切り下げが十分に行われるため、干渉によって回らないという事態は避けられている。
バックラッシュ†
バックラッシュは計算済みの球面インボリュート曲線と球面トロコイド曲線を円周方向に移動することで実現した。
転位†
未対応
まがりばかさ歯車†
歯溝形状をロフトによって繋いで歯形を作る際に、回転軸に沿って回転しながら掃引することでまがりばかさ歯車を生成できる。
かさば歯車は外端の基準円の半径が $mz/2$ であり、基準円から歯面に沿って中心点へ延びる距離が $r_0=(mz/2)/\sin\gamma_p$ であるから、歯面に沿った歯幅を $w$ とすれば外端に作った歯溝形状パッチを中心点を基準に $(r_0-w)/r_0$ 倍だけ縮めたところまで歯溝形状を掃引することになる。
この間、倍率を $s$ とすると、倍率の変化 $ds$ に対して歯面上では $r_0ds$ だけ移動するから、まがり歯の角度を $\beta$ とするとこの間に回転軸を中心に $d\theta$ 回すとすると、 $$ \tan\beta=\frac{(mz/2)sd\theta}{r_0ds} $$ の関係が保たれる必要がある。変形して、 $$ \frac{d\theta}{ds}=\frac{2r_0\tan\beta}{mzs} $$ であるから、これを積分することでまがり歯形状を表す式 $$ \theta(s)=\frac{2r_0\tan\beta}{mz}\log(s) $$ を得る。
スケールに対して回転角が対数関数 $\log$ で決まるのは考えてみれば当然か。
例えば半径 $a$ のものを $1/2$ 倍したのと、2回 $1/\sqrt2$ 倍したのとは同じ半径 $a/2$ になるのだけれど、その両者でに回転角が一致するには回転角がスケールの対数関数 $\log$ になっていないといけない。
逆にちゃんと $\log$ になっていれば、曲線状のどの点をスケール&回転してもちゃんと曲線上に載ってくれる。
かさばの内歯車?†
インボリュート平歯車を反転することで内歯車になったのと同様に、 球面インボリュートかさば歯車を反転すればかさばの内歯車が作れる気がする?
軸間の角度は $\gamma_{p1}+\gamma_{p2}$ ではなく $\gamma_{p1}-\gamma_{p2}$ になるのかな。



