2次曲線
曲線・曲面の数式表現 では、次数が2の代数曲線を2次曲線と呼び、円・楕円・放物線・双曲線がこれにあたることに触れました。
見た目の違うこの4つの図形が2次曲線という1つのカテゴリにまとめられるのは、係数を変えるだけで同じ式に収まり、しかも同じ図形から切り出せるからです。
2次曲線の一般式
陰関数形式 では、座標が満たすべき条件を方程式として与え、それを満たす点の集まりを曲線としました。その左辺を と の2次多項式にしたものが2次曲線で、式は次の形になります。
円・楕円・放物線・双曲線の違いは、この6つの係数 の値の違いでしかありません。
から までの6つの係数のうち、2次の項につく は、少なくとも1つが0でなければなりません。3つとも0だと式は1次になり、直線になってしまいます。
円錐曲線
また、円・楕円・放物線・双曲線は、いずれも円錐面を平面で切った切り口として現れます。
円錐面は、母線と呼ばれる1本の直線を、それと1点で交わる軸のまわりに回すことでみられる図形です。底面を持つ立体の円錐とは異なり、円錐面は軸との交点を挟んで上下2つに分かれ、上下どちら側にも限りなく広がっていきます。
回転角を動かして、1本の母線が動いた跡として円錐面が描かれていく様子を見てみよう
Three.jsによる実装概要
// 母線を軸のまわりに回した角度
const rotation = (ROTATION_DEG * Math.PI) / 180
// 半頂角 α の母線を軸(y 軸)のまわりに θ だけ回したときの、パラメータ t の点。
// t は頂点からの軸方向の距離で、負にとると頂点を挟んで反対側の面になる
const generatrixPoint = (theta: number, t: number, target: Vector3) =>
target.set(t * TAN_HALF_ANGLE * Math.cos(theta), t, t * TAN_HALF_ANGLE * Math.sin(theta))
// 母線を回すもとになる軸
const axisGeometry = new BufferGeometry().setFromPoints([
new Vector3(0, -AXIS_EXTENT, 0),
new Vector3(0, AXIS_EXTENT, 0)
])
const axisMaterial = new LineBasicMaterial({ color: AXIS_COLOR })
scene.add(new LineSegments(axisGeometry, axisMaterial))
// 1 周まで回したときに母線の端が届く輪。掃かれた跡がどこへ向かっているかの目安になる
const rimPoints: Vector3[] = []
for (let i = 0; i < RIM_SEGMENTS; i++) {
const angle = (i / RIM_SEGMENTS) * Math.PI * 2
rimPoints.push(new Vector3(CONE_RADIUS * Math.cos(angle), 0, CONE_RADIUS * Math.sin(angle)))
}
const rimGeometry = new BufferGeometry().setFromPoints(rimPoints)
const rimMaterial = new LineBasicMaterial({ color: RIM_COLOR })
for (const y of [CONE_EXTENT, -CONE_EXTENT]) {
const rim = new LineLoop(rimGeometry, rimMaterial)
rim.position.y = y
scene.add(rim)
}
// 母線が通った跡としての円錐面。母線は直線なので、
// 刻んだ角度ごとに両端(t = ±CONE_EXTENT)の 2 点を置き、隣り合う母線のあいだを四角形で張る
const surfacePositions = new Float32BufferAttribute(
new Float32Array((SWEEP_SEGMENTS + 1) * 2 * 3),
3
)
const surfaceGeometry = new BufferGeometry().setAttribute("position", surfacePositions)
const indices: number[] = []
for (let i = 0; i < SWEEP_SEGMENTS; i++) {
const lower = i * 2
const upper = i * 2 + 1
indices.push(lower, upper, upper + 2, lower, upper + 2, lower + 2)
}
surfaceGeometry.setIndex(indices)
const surfaceMaterial = new MeshBasicMaterial({
color: SURFACE_COLOR,
side: DoubleSide,
transparent: true,
opacity: SURFACE_OPACITY,
// 面の向こう側にある母線や軸を隠したくないので、深度は比較するが書かない
depthWrite: false
})
scene.add(new Mesh(surfaceGeometry, surfaceMaterial))
// 0 から θ まで回したぶんの面を張る
const point = new Vector3()
for (let i = 0; i <= SWEEP_SEGMENTS; i++) {
const angle = (rotation * i) / SWEEP_SEGMENTS
generatrixPoint(angle, -CONE_EXTENT, point)
surfacePositions.setXYZ(i * 2, point.x, point.y, point.z)
generatrixPoint(angle, CONE_EXTENT, point)
surfacePositions.setXYZ(i * 2 + 1, point.x, point.y, point.z)
}
surfacePositions.needsUpdate = true
// 母線の端(パラメータ t の位置)が通った跡の弧。掃かれた面の縁にあたる
const addSweptArc = (t: number) => {
const positions = new Float32BufferAttribute(new Float32Array((RIM_SEGMENTS + 1) * 3), 3)
const geometry = new BufferGeometry().setAttribute("position", positions)
const material = new LineBasicMaterial({ color: SURFACE_COLOR })
scene.add(new Line(geometry, material))
for (let i = 0; i <= RIM_SEGMENTS; i++) {
generatrixPoint((rotation * i) / RIM_SEGMENTS, t, point)
positions.setXYZ(i, point.x, point.y, point.z)
}
positions.needsUpdate = true
}
addSweptArc(CONE_EXTENT)
addSweptArc(-CONE_EXTENT)
// 母線 1 本。頂点を通り、パラメータ t が ±CONE_EXTENT まで伸びる
const addGeneratrix = (theta: number, opacity: number) => {
const positions = new Float32BufferAttribute(new Float32Array(6), 3)
const geometry = new BufferGeometry().setAttribute("position", positions)
const material = new LineBasicMaterial({
color: GENERATRIX_COLOR,
transparent: opacity < 1,
opacity
})
scene.add(new LineSegments(geometry, material))
generatrixPoint(theta, -CONE_EXTENT, point)
positions.setXYZ(0, point.x, point.y, point.z)
generatrixPoint(theta, CONE_EXTENT, point)
positions.setXYZ(1, point.x, point.y, point.z)
positions.needsUpdate = true
}
// 回しはじめ(θ = 0)の母線は薄く残し、今の母線と見比べられるようにする
addGeneratrix(0, START_OPACITY)
addGeneratrix(rotation, 1)円錐面を平面で切ったときにどの曲線が現れるかは、母線の傾きと切る平面の傾きの関係で決まります。
- 母線よりゆるやかな傾きで切ると、すべての母線と交わり、閉じた楕円になる(軸に垂直な切り方をすると円になる)
- 母線と平行に切ると、切り口は閉じずにどこまでも伸び、放物線になる
- 母線より急な傾きで切ると、頂点の上下どちらの側とも交わり、
2本の枝に分かれた双曲線になる
いずれも同じく円錐面の断面に現れるため、2次曲線は円錐曲線とも呼ばれます。
平面の傾きを動かして、切り口に現れる曲線が母線の傾きを境にどう変わるか見てみよう
Three.jsによる実装概要
// 円錐面。半頂角 α の母線を軸(y 軸)のまわりに回した面を、頂点を挟んで上下 2 つ置く。
// ConeGeometry は頂点が +y 側なので、上側は反転させてどちらも頂点を原点に合わせる
const coneGeometry = new ConeGeometry(CONE_RADIUS, CONE_EXTENT, 48, 1, true)
const coneMaterial = new MeshBasicMaterial({
color: CONE_COLOR,
side: DoubleSide,
transparent: true,
opacity: CONE_OPACITY,
// 面の内側にある切り口を隠したくないので、深度は比較するが書かない
depthWrite: false
})
const upperCone = new Mesh(coneGeometry, coneMaterial)
upperCone.rotation.x = Math.PI
upperCone.position.y = CONE_EXTENT / 2
scene.add(upperCone)
const lowerCone = new Mesh(coneGeometry, coneMaterial)
lowerCone.position.y = -CONE_EXTENT / 2
scene.add(lowerCone)
// 円錐面をどこまで描いたかを示す輪。切り口がここで途切れるのは、面の広がりを有限に描いているため
const ringPoints: Vector3[] = []
for (let i = 0; i < RING_SEGMENTS; i++) {
const theta = (i / RING_SEGMENTS) * Math.PI * 2
ringPoints.push(new Vector3(CONE_RADIUS * Math.cos(theta), 0, CONE_RADIUS * Math.sin(theta)))
}
const ringGeometry = new BufferGeometry().setFromPoints(ringPoints)
const ringMaterial = new LineBasicMaterial({ color: CONE_COLOR })
for (const y of [CONE_EXTENT, -CONE_EXTENT]) {
const ring = new LineLoop(ringGeometry, ringMaterial)
ring.position.y = y
scene.add(ring)
}
// 傾きを比べる母線。切り口を隠さないよう、平面が倒れていく向き(+x 側)の 1 本だけを描く。
// この母線と平面はどちらも xy 平面に乗るので、角度をそのまま見比べられる
const generatrixGeometry = new BufferGeometry().setFromPoints([
new Vector3(-CONE_RADIUS, -CONE_EXTENT, 0),
new Vector3(CONE_RADIUS, CONE_EXTENT, 0)
])
const generatrixMaterial = new LineBasicMaterial({ color: GENERATRIX_COLOR })
scene.add(new LineSegments(generatrixGeometry, generatrixMaterial))
// 母線を回すもとになった軸
const axisGeometry = new BufferGeometry().setFromPoints([
new Vector3(0, -AXIS_EXTENT, 0),
new Vector3(0, AXIS_EXTENT, 0)
])
const axisMaterial = new LineBasicMaterial({ color: AXIS_COLOR })
scene.add(new LineSegments(axisGeometry, axisMaterial))
// 切る平面。(0, PLANE_HEIGHT, 0) を通り、そこを中心に z 軸まわりへ傾ける
const tilt = (TILT_DEG * Math.PI) / 180
const planeGroup = new Group()
planeGroup.position.y = PLANE_HEIGHT
planeGroup.rotation.z = tilt
scene.add(planeGroup)
const planeGeometry = new PlaneGeometry(PLANE_SIZE, PLANE_SIZE)
const planeMaterial = new MeshBasicMaterial({
color: PLANE_COLOR,
side: DoubleSide,
transparent: true,
opacity: PLANE_OPACITY,
depthWrite: false
})
const plane = new Mesh(planeGeometry, planeMaterial)
// PlaneGeometry は xy 平面に作られるので、寝かせて法線を軸の向きに合わせる
plane.rotation.x = -Math.PI / 2
planeGroup.add(plane)
// 切り口。線分の本数は刻み数までなので、その分の場所を先に確保しておく
const sectionPositions = new Float32BufferAttribute(new Float32Array(SECTION_SEGMENTS * 2 * 3), 3)
const sectionGeometry = new BufferGeometry().setAttribute("position", sectionPositions)
const sectionMaterial = new LineBasicMaterial({ color: SECTION_COLOR })
scene.add(new LineSegments(sectionGeometry, sectionMaterial))
const cosTilt = Math.cos(tilt)
const sinTilt = Math.sin(tilt)
// θ ごとに求めた切り口の点と、それが円錐面を描いた範囲に入っているか
const samples = new Float32Array((SECTION_SEGMENTS + 1) * 3)
const insideCone = new Uint8Array(SECTION_SEGMENTS + 1)
// 円錐面上の点 (t tanα cosθ, t, t tanα sinθ) を平面の式に入れると、θ ごとに t が 1 つ決まる
for (let i = 0; i <= SECTION_SEGMENTS; i++) {
const theta = (i / SECTION_SEGMENTS) * Math.PI * 2
const denominator = cosTilt - sinTilt * TAN_HALF_ANGLE * Math.cos(theta)
const t = (cosTilt * PLANE_HEIGHT) / denominator
samples[i * 3] = t * TAN_HALF_ANGLE * Math.cos(theta)
samples[i * 3 + 1] = t
samples[i * 3 + 2] = t * TAN_HALF_ANGLE * Math.sin(theta)
// 分母が 0 に近いところは、切り口が無限の彼方へ逃げている
insideCone[i] = Math.abs(denominator) > DENOMINATOR_EPSILON && Math.abs(t) <= CONE_EXTENT ? 1 : 0
}
// 隣り合う点を結ぶ。分母の符号が変わったところは頂点を挟んで反対側の円錐へ飛んでいるので繋がない
let segmentCount = 0
for (let i = 0; i < SECTION_SEGMENTS; i++) {
const next = i + 1
if (!insideCone[i] || !insideCone[next]) continue
if (Math.sign(samples[i * 3 + 1]) !== Math.sign(samples[next * 3 + 1])) continue
sectionPositions.setXYZ(segmentCount * 2, samples[i * 3], samples[i * 3 + 1], samples[i * 3 + 2])
sectionPositions.setXYZ(
segmentCount * 2 + 1,
samples[next * 3],
samples[next * 3 + 1],
samples[next * 3 + 2]
)
segmentCount++
}
sectionPositions.needsUpdate = true
sectionGeometry.setDrawRange(0, segmentCount * 2)
// 切る平面の傾きを母線の傾きと比べて、現れる曲線の名前を決める
const nameSection = (tiltDeg: number) => {
if (Math.abs(tiltDeg - GENERATRIX_TILT_DEG) < TILT_EPSILON) return "放物線"
if (tiltDeg > GENERATRIX_TILT_DEG) return "双曲線"
if (tiltDeg < TILT_EPSILON) return "円"
return "楕円"
} 標準形
同じ形・同じ大きさの楕円でも、置かれた場所や向きが違えば、一般式の係数は別の値になります。たとえば、原点を中心に置いた楕円を平行移動や回転 すると、係数が0から変化し、新たな項が式に現れることがあります。
このような平行移動や回転を取り消し、中心を原点に合わせて対称軸を座標軸に重ねると、よりシンプルな式で表すことができます。こうして位置と向きの違いを取り除いた式を標準形といいます。
平行移動や回転を取り消して標準形にするには、その逆方向への平行移動や回転を行えばよいのです。つまり、どんな位置・向きに置かれた2次曲線も、平行移動と回転によって標準形に変換できます。
楕円
楕円の標準形の式は、円の式を軸ごとに引き伸ばしたものとして導かれます。
半径 の円の式 の両辺を で割ると、 となります。 も も同じ で割ることで、縦にも横にも同じだけ拡大されます。
を に置き換えた式が成り立つのは、 の値が元の式を満たすときです。
元の図形上の点と同じ値を がとるのは がその点の 倍の位置にあるときなので、置き換えた式は、元の図形を 方向へ 倍に引き伸ばした図形を表します。
楕円の式は、ここから 側の分母だけを に取り替えたものです。
横方向の拡大率 と縦方向の拡大率 が異なる値となることで、円を縦横どちらかに大きく引き伸ばした形、すなわち楕円になります。
楕円の標準形
と は、中心から曲線までの 軸方向・ 軸方向の距離。等しいときが半径 の円
放物線
放物線は、1つの定点と1本の定直線から等しい距離にある点の集まりとしても定義できます。この定点を焦点、定直線を準線といい、頂点を原点に置いて焦点を にとると、準線は になります。
ピンクの点と水色の点を結ぶ2線が常に同じ長さであることに注目しながら、pやyを動かしてみよう
Three.jsによる実装概要
// 放物線 y² = 4px 上で、高さ y にある点の x
const parabolaX = (y: number) => (y * y) / (4 * P_CURRENT)
// 焦点と準線から等しい距離にある点の集まりとして現れる放物線。
// y を刻んで、その高さでの x を並べた折れ線として描く
const positions = new Float32BufferAttribute(new Float32Array((CURVE_SEGMENTS + 1) * 3), 3)
const geometry = new BufferGeometry().setAttribute("position", positions)
const material = new LineBasicMaterial({ color: CURVE_COLOR })
scene.add(new Line(geometry, material))
for (let i = 0; i <= CURVE_SEGMENTS; i++) {
const y = -CURVE_Y_MAX + 2 * CURVE_Y_MAX * (i / CURVE_SEGMENTS)
positions.setXYZ(i, parabolaX(y), y, LAYER_CURVE)
}
positions.needsUpdate = true
// 両端を指定して引く 1 本の線分
const addSegment = (
color: string,
z: number,
fromX: number,
fromY: number,
toX: number,
toY: number
) => {
const geometry = new BufferGeometry().setFromPoints([
new Vector3(fromX, fromY, z),
new Vector3(toX, toY, z)
])
const material = new LineBasicMaterial({ color })
scene.add(new LineSegments(geometry, material))
}
// 特定の位置を示す球
const addMarker = (color: string, radius: number, x: number, y: number) => {
const geometry = new SphereGeometry(radius, 16, 12)
const material = new MeshBasicMaterial({ color })
const marker = new Mesh(geometry, material)
marker.position.set(x, y, LAYER_POINT)
scene.add(marker)
}
// 放物線を決める 1 組。定点である焦点 (p, 0) と、定直線である準線 x = -p
addMarker(DEFINITION_COLOR, ANCHOR_RADIUS, P_CURRENT, 0)
addSegment(DEFINITION_COLOR, LAYER_HELPER, -P_CURRENT, -AXIS_HALF_Y, -P_CURRENT, AXIS_HALF_Y)
// 曲線上で見ている点と、準線の上でその点から下ろした垂線が当たる位置
const x = parabolaX(Y_CURRENT)
addMarker(POINT_COLOR, POINT_RADIUS, x, Y_CURRENT)
addMarker(DEFINITION_COLOR, ANCHOR_RADIUS, -P_CURRENT, Y_CURRENT)
// 点から焦点・準線へ引いた 2 本の線。長さが等しいことが主題なので、2 本は同じ色にする
addSegment(DISTANCE_COLOR, LAYER_DISTANCE, x, Y_CURRENT, P_CURRENT, 0)
addSegment(DISTANCE_COLOR, LAYER_DISTANCE, x, Y_CURRENT, -P_CURRENT, Y_CURRENT)
// 焦点までは 2 点間の距離、準線までは x 方向の隔たり。
// 別々に求めているが、放物線上の点ではこの 2 つが必ず一致する
const focusDistance = Math.hypot(x - P_CURRENT, Y_CURRENT)
const directrixDistance = x + P_CURRENTこの2つへの距離が等しいという条件を式にすると となり、 と が打ち消し合って が残ります。
曲線上の点 から焦点 までは、2点間の距離として と書けます。
準線 までの距離は、点から準線へ下ろした垂線の長さです。準線が 軸と平行なため垂線は横向きになり、その長さは 方向の隔たり だけで求まります。
この2つを等しいと置いて両辺を2乗すると、根号が外れて が得られます。
放物線の標準形
原点が頂点で、 軸が対称軸。 が大きいほど開き方はゆるやか
放物線は、対称軸に関して線対称な曲線です。頂点を原点に置いて対称軸を 軸に重ねると、同じ に対して と が常に対になって現れます。 の符号を入れ替えても同じように成り立つ式でなければならないので、 は2乗の形でしか式に入れません。
また、この は に比例する形になっています。比例定数は であるため、 が大きいほど同じ に対して が大きく広がり、開き方がゆるやかになります。
双曲線
双曲線の標準形は、楕円の標準形の足し算を引き算に変えた形です。
双曲線の標準形
軸上で原点から だけ離れた2点を頂点として、左右2本の枝に分かれる
と書き直すと、右辺は必ず1以上になります。
2乗の大小は、絶対値の大小と対応します。2乗すると符号が消えて原点からの距離だけが残り、その距離が大きいほど2乗も大きくなるためです。 と の比較も、 が原点のどちら側にあるかを問わず、 の絶対値と の比較として読み替えられます。
が より小さいと左辺が1未満になってしまうため、 の絶対値は より小さくなれず、直線 の右側と の左側に曲線が分かれます。
また、 を大きくしていくと右辺の1が効かなくなり、曲線は という2本の直線に限りなく近づいていきます。このように、曲線が限りなく近づいていく直線を漸近線といいます。
次の様子に注目して、デモを動かしてみよう
aを動かすと、曲線が入ってこられない領域の幅が変化するyを大きくしていくと、曲線と直線がほぼ重なりそうなくらい近づいていく
Three.jsによる実装概要
// 標準形 x²/a² - y²/b² = 1 の右側の枝で、高さ y にある点の x
const hyperbolaX = (y: number) => A_CURRENT * Math.sqrt(1 + (y * y) / (RADIUS_B * RADIUS_B))
// 漸近線 y = ±(b/a)x を x について解いた形。高さ y にある点の x
const asymptoteX = (y: number) => (A_CURRENT * y) / RADIUS_B
// y を刻んで、その高さでの x を並べた折れ線
const addCurve = (color: string, z: number, toX: (y: number) => number) => {
const positions = new Float32BufferAttribute(new Float32Array((CURVE_SEGMENTS + 1) * 3), 3)
const geometry = new BufferGeometry().setAttribute("position", positions)
const material = new LineBasicMaterial({ color })
scene.add(new Line(geometry, material))
for (let i = 0; i <= CURVE_SEGMENTS; i++) {
const y = -CURVE_Y_MAX + 2 * CURVE_Y_MAX * (i / CURVE_SEGMENTS)
positions.setXYZ(i, toX(y), y, z)
}
positions.needsUpdate = true
}
// 両端を指定して引く 1 本の線分
const addSegment = (
color: string,
z: number,
fromX: number,
fromY: number,
toX: number,
toY: number
) => {
const geometry = new BufferGeometry().setFromPoints([
new Vector3(fromX, fromY, z),
new Vector3(toX, toY, z)
])
const material = new LineBasicMaterial({ color })
scene.add(new LineSegments(geometry, material))
}
// 特定の位置を示す球
const addMarker = (color: string, radius: number, x: number, y: number) => {
const geometry = new SphereGeometry(radius, 16, 12)
const material = new MeshBasicMaterial({ color })
const marker = new Mesh(geometry, material)
marker.position.set(x, y, LAYER_POINT)
scene.add(marker)
}
// x の絶対値が a より小さくなれない、すなわち曲線が入ってこられない帯とその境界
const bandGeometry = new PlaneGeometry(A_CURRENT * 2, VERTICAL_HALF * 2)
const bandMaterial = new MeshBasicMaterial({
color: HELPER_COLOR,
transparent: true,
opacity: BAND_OPACITY,
depthWrite: false
})
const band = new Mesh(bandGeometry, bandMaterial)
band.position.z = LAYER_BAND
scene.add(band)
addSegment(HELPER_COLOR, LAYER_HELPER, A_CURRENT, -VERTICAL_HALF, A_CURRENT, VERTICAL_HALF)
addSegment(HELPER_COLOR, LAYER_HELPER, -A_CURRENT, -VERTICAL_HALF, -A_CURRENT, VERTICAL_HALF)
// y を大きくしていったときに曲線が限りなく近づく 2 本の直線。
// どちらも原点を通る直線なので、上端と下端を結ぶだけでよい
const asymptoteTop = asymptoteX(CURVE_Y_MAX)
addSegment(ASYMPTOTE_COLOR, LAYER_ASYMPTOTE, -asymptoteTop, -CURVE_Y_MAX, asymptoteTop, CURVE_Y_MAX)
addSegment(ASYMPTOTE_COLOR, LAYER_ASYMPTOTE, asymptoteTop, -CURVE_Y_MAX, -asymptoteTop, CURVE_Y_MAX)
// 帯を挟んで右と左に分かれる 2 本の枝
addCurve(CURVE_COLOR, LAYER_CURVE, hyperbolaX)
addCurve(CURVE_COLOR, LAYER_CURVE, (y) => -hyperbolaX(y))
// 曲線上の点と、同じ高さで漸近線の上にある点。
// 高さが負のときに右の枝が近づく先は、傾きが負のほうの漸近線なので、絶対値で見る
const curveX = hyperbolaX(Y_CURRENT)
const footX = asymptoteX(Math.abs(Y_CURRENT))
addMarker(POINT_COLOR, POINT_RADIUS, curveX, Y_CURRENT)
addMarker(POINT_COLOR, POINT_RADIUS, -curveX, Y_CURRENT)
addMarker(ASYMPTOTE_COLOR, ANCHOR_RADIUS, footX, Y_CURRENT)
addMarker(ASYMPTOTE_COLOR, ANCHOR_RADIUS, -footX, Y_CURRENT)
// その高さでの、曲線と漸近線の隔たり。y を大きくするほど 0 へ向かう
addSegment(GAP_COLOR, LAYER_GAP, footX, Y_CURRENT, curveX, Y_CURRENT)
addSegment(GAP_COLOR, LAYER_GAP, -footX, Y_CURRENT, -curveX, Y_CURRENT)
const gap = curveX - footX パラメータ形式
標準形まで簡単にしても、 を決めて を求めるには方程式を解くことになり、曲線上の点を順に並べるのは大変です。そこで、パラメータ形式 で書き直します。
楕円
楕円は、角度 をパラメータにとります。
楕円のパラメータ形式
は中心から曲線までの 軸方向・ 軸方向の距離。 が一周すると楕円を1周する
この式は、半径1の円周上の点の と を、それぞれ 倍・ 倍したものと読むこともできます。
縦横で倍率が異なるので、得られるのは正円ではなく、引き伸ばされた楕円です。
と が等しいときだけ倍率が揃い、半径 の円となります。
また、このパラメータ形式は、標準形の式と次のように繋がります。
まず標準形の左辺を、 と をそれぞれ と で割った量の2乗の和として書き直します。
パラメータ形式より、 なので、代入すると、
この式は、どの であっても成り立つ、有名な三角関数の関係式です。
と は、半径1の円周上で角度 の位置にある点の 座標と 座標なので、2乗の和は原点からその点までの距離(半径)の2乗、つまり1になる、と捉えることができます。
三角関数の関係式
楕円の標準形
放物線
放物線は、パラメータを として次のように書けます。
放物線のパラメータ形式
は頂点から焦点までの距離。 の符号が、対称軸を挟んで上下どちら側の点かに対応する
は に比例して増える一方、 は の2乗で増えるので、縦方向に一定の割合で進む間に、横方向にはその2乗で離れていきます。頂点の近くではほとんど横に動かず、離れるほど横へ大きく開いていく放物線の形が、そのまま式に出ています。
この式は、標準形から導くこともできます。 に を代入すると左辺が になり、両辺を で割ると が残ります。 の置き方に 倍を選んでおくのは、 の式に分数を出さないためです。
の符号を入れ替えると、 は変わらないまま だけが符号を変えます。
標準形で を2乗の形でしか使わなかった線対称性が、パラメータ形式では と の対として現れます。
双曲線
楕円のパラメータ形式では三角関数を使いましたが、双曲線では双曲線関数を使います。
双曲線のパラメータ形式
は原点から頂点までの距離、 は曲線が近づく直線の傾き。辿れるのは が正となる側の枝だけ
三角関数が単位円上を巡るのに対して、双曲線関数は双曲線上を辿ります。
双曲線関数 と は、指数関数を組み合わせて定義される関数です。
これらの間には という関係があり、楕円の場合と同様の考え方で、標準形の左辺にそのまま当てはまります。
双曲線関数の関係式
双曲線の標準形
ただし、 は常に1以上の値をとるので、 という式で表せるのは が正となる側の曲線だけです。 が負となる側の曲線は、 と符号を反転させることで得られます。
tを動かして、cosh t・sinh tの値と、点(x, y)との対応を観察しよう
cosh tの値が上下するとxが、sinh tの値が上下するとyが動くtをいくら動かしても、y軸より右側の双曲線しか描かれない- 符号を逆転で
cosh tが-cosh tに反転し、y軸より左側の双曲線が描かれるようになる
Three.jsによる実装概要
// cosh t = (eᵗ + e⁻ᵗ) / 2
const cosh = (t: number) => (Math.exp(t) + Math.exp(-t)) / 2
// sinh t = (eᵗ − e⁻ᵗ) / 2
const sinh = (t: number) => (Math.exp(t) - Math.exp(-t)) / 2
// t を刻んで点を並べた折れ線。toPoint は t からその図での位置を返す
const addParametricCurve = (
parent: Object3D,
color: string,
z: number,
from: number,
to: number,
toPoint: (t: number, target: Vector3) => Vector3
) => {
const positions = new Float32BufferAttribute(new Float32Array((CURVE_SEGMENTS + 1) * 3), 3)
const geometry = new BufferGeometry().setAttribute("position", positions)
const material = new LineBasicMaterial({ color })
parent.add(new Line(geometry, material))
const point = new Vector3()
for (let i = 0; i <= CURVE_SEGMENTS; i++) {
toPoint(from + (to - from) * (i / CURVE_SEGMENTS), point)
positions.setXYZ(i, point.x, point.y, z)
}
positions.needsUpdate = true
}
// 「符号を逆転」で切り替える、辿っている枝の向き。
// 左のグラフの cosh t も同じ符号で反転させ、点の x に効いている値をそのまま見せる
const sign = FLIP ? -1 : 1
// 左:t を横軸にとった cosh t と sinh t のグラフ
const graph = new Group()
graph.position.x = GRAPH_CENTER_X
scene.add(graph)
addParametricCurve(graph, X_COLOR, LAYER_CURVE, -T_MAX, T_MAX, (t, target) =>
target.set(t * GRAPH_SCALE_T, sign * cosh(t) * GRAPH_SCALE_VALUE, 0)
)
addParametricCurve(graph, Y_COLOR, LAYER_CURVE, -T_MAX, T_MAX, (t, target) =>
target.set(t * GRAPH_SCALE_T, sinh(t) * GRAPH_SCALE_VALUE, 0)
)
// 右:cosh・sinh の値をそのまま座標にとった双曲線
const curve = new Group()
curve.position.x = CURVE_CENTER_X
scene.add(curve)
// x = a cosh t, y = b sinh t で辿れる枝と、符号を反転させないと得られないもう 1 本の枝
addParametricCurve(curve, CURVE_COLOR, LAYER_CURVE, -T_MAX, T_MAX, (t, target) =>
target.set(RADIUS_A * cosh(t) * CURVE_SCALE, RADIUS_B * sinh(t) * CURVE_SCALE, 0)
)
addParametricCurve(curve, CURVE_COLOR, LAYER_CURVE, -T_MAX, T_MAX, (t, target) =>
target.set(-RADIUS_A * cosh(t) * CURVE_SCALE, RADIUS_B * sinh(t) * CURVE_SCALE, 0)
)
// t をここまで動かしたぶんの跡
addParametricCurve(curve, TRACE_COLOR, LAYER_TRACE, -T_MAX, T_CURRENT, (t, target) =>
target.set(sign * RADIUS_A * cosh(t) * CURVE_SCALE, RADIUS_B * sinh(t) * CURVE_SCALE, 0)
)
// 両端が動く 1 本の線分
const addSegment = (
parent: Object3D,
color: string,
opacity: number,
fromX: number,
fromY: number,
toX: number,
toY: number
) => {
const geometry = new BufferGeometry().setFromPoints([
new Vector3(fromX, fromY, LAYER_HELPER),
new Vector3(toX, toY, LAYER_HELPER)
])
const material = new LineBasicMaterial({ color, transparent: opacity < 1, opacity })
parent.add(new LineSegments(geometry, material))
}
// cosh がここより下へ来ないことを示す、値 1 の高さの線と、
// そのため x が a より内側へ来ないことを示す縦の線。どちらも符号を逆転させると反対側へ移る
addSegment(graph, HELPER_COLOR, HELPER_OPACITY, -GRAPH_HALF_T, sign * GRAPH_SCALE_VALUE, GRAPH_HALF_T, sign * GRAPH_SCALE_VALUE)
addSegment(curve, HELPER_COLOR, HELPER_OPACITY, sign * RADIUS_A * CURVE_SCALE, -CURVE_HALF_Y, sign * RADIUS_A * CURVE_SCALE, CURVE_HALF_Y)
// 今の t を示す縦線と、その t での cosh・sinh の値
const coshValue = cosh(T_CURRENT)
const sinhValue = sinh(T_CURRENT)
const graphX = T_CURRENT * GRAPH_SCALE_T
addSegment(graph, HELPER_COLOR, HELPER_OPACITY, graphX, -GRAPH_HALF_VALUE, graphX, GRAPH_HALF_VALUE)
const addMarker = (parent: Object3D, color: string, x: number, y: number) => {
const geometry = new SphereGeometry(POINT_RADIUS, 16, 12)
const material = new MeshBasicMaterial({ color })
const marker = new Mesh(geometry, material)
marker.position.set(x, y, LAYER_POINT)
parent.add(marker)
}
addMarker(graph, X_COLOR, graphX, sign * coshValue * GRAPH_SCALE_VALUE)
addMarker(graph, Y_COLOR, graphX, sinhValue * GRAPH_SCALE_VALUE)
// 右の図での、今の t に対応する点と、その座標を軸の上で読むための線
const x = sign * RADIUS_A * coshValue * CURVE_SCALE
const y = RADIUS_B * sinhValue * CURVE_SCALE
addMarker(curve, MARKER_COLOR, x, y)
addSegment(curve, X_COLOR, HELPER_OPACITY, x, y, x, 0)
addSegment(curve, Y_COLOR, HELPER_OPACITY, x, y, 0, y)