モデルの二階層構造
The page you are currently viewing does not support the selected display language.
ロボットなどのようにいくつかのパーツに分かれているようなモデルは、手や足などのパーツが親の動きに従って一緒に動作しています。また、手や足などは単独で回転したりします。これは「親子関係」という構造化を実現することにより可能になります。
操作は親のボックス(赤)を「↑」「↓」キーで X 軸回転、「←」「→」キーで Z 軸回転します。全てワールド座標で回転するので操作しやすいかと思います。
子のボックスは自動で親のボックスの先で回転するようになっています。親を動かすと子も一緒に移動することがわかります。
今回のメインコードファイルを載せます。
MainSample.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace MDXSample
{
<summary>
メインサンプルクラス
</summary>
public partial class MainSample : IDisposable
{
<summary>
ボックスメッシュ
</summary>
private Mesh _box = null;
<summary>
親ボックスの回転マトリックス
</summary>
private Matrix _parentBoxRotate = Matrix.Identity;
<summary>
アプリケーションの初期化
</summary>
<param name="topLevelForm">トップレベルウインドウ</param>
<returns>全ての初期化がOKなら true, ひとつでも失敗したら false を返すようにする</returns>
<remarks>
false を返した場合は、自動的にアプリケーションが終了するようになっている
</remarks>
public bool InitializeApplication(MainForm topLevelForm)
{
// フォームの参照を保持
this._form = topLevelForm;
// 入力イベント作成
this.CreateInputEvent(topLevelForm);
try
{
// Direct3D デバイス作成
this.CreateDevice(topLevelForm);
// フォントの作成
this.CreateFont();
// XYZライン作成
this.CreateXYZLine();
}
catch (DirectXException ex)
{
// 例外発生
MessageBox.Show(ex.ToString(), "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
// カメラの位置セット
this.SetCameraPosition(8.0f, 270.0f, 30.0f);
// ボックス作成
this._box = Mesh.Box(this._device, 0.5f, 2.0f, 0.5f);
// ライトの設定
this.SettingLight();
return true;
}
<summary>
デバイスがロストしたとき
</summary>
<param name="sender"></param>
<param name="e"></param>
private void device_DeviceLost(object sender, EventArgs e)
{
}
<summary>
デバイスがリセットしたとき
</summary>
<param name="sender"></param>
<param name="e"></param>
private void device_DeviceReset(object sender, EventArgs e)
{
}
<summary>
更新処理
</summary>
public void Update()
{
// アプリケーションの終了操作
if (this._keys[(int)Keys.Escape])
{
this._form.Close();
return;
}
// カメラの設定
this.SettingCamera();
// 親ボックスの回転
if (this._keys[(int)Keys.Down])
{
this._parentBoxRotate *= Matrix.RotationX(-0.1f);
}
if (this._keys[(int)Keys.Up])
{
this._parentBoxRotate *= Matrix.RotationX(0.1f);
}
if (this._keys[(int)Keys.Left])
{
this._parentBoxRotate *= Matrix.RotationZ(0.1f);
}
if (this._keys[(int)Keys.Right])
{
this._parentBoxRotate *= Matrix.RotationZ(-0.1f);
}
}
<summary>
描画処理
</summary>
public void Draw()
{
// デバイスが使える状態か確認する
if (!this.EnsureDevice())
{
return;
}
// 描画内容を単色でクリアし、Zバッファもクリア
this._device.Clear(ClearFlags.ZBuffer | ClearFlags.Target, Color.DarkBlue, 1.0f, 0);
// 「BeginScene」と「EndScene」の間に描画内容を記述する
this._device.BeginScene();
// XYZライン描画
this.RenderXYZLine();
// マテリアル初期化
Material mtrl = new Material();
mtrl.Ambient = Color.FromArgb(255, 128, 128, 128);
// マテリアル
mtrl.Diffuse = Color.Red;
this._device.Material = mtrl;
// 親のマトリックス初期化
Matrix parentBoxMatrix = Matrix.Identity;
// 回転軸移動
parentBoxMatrix *= Matrix.Translation(0.0f, 1.0f, 0.0f);
// 回転
parentBoxMatrix *= this._parentBoxRotate;
// 座標変換
this._device.SetTransform(TransformType.World, parentBoxMatrix);
// 親ボックス描画
this._box.DrawSubset(0);
// マテリアル
mtrl.Diffuse = Color.Blue;
this._device.Material = mtrl;
// 子のマトリックス初期化
Matrix childBoxMatrix = Matrix.Identity;
// 回転軸移動
childBoxMatrix *= Matrix.Translation(0.0f, 1.0f, 0.0f);
// 初期回転位置(傾き)
childBoxMatrix *= Matrix.RotationZ((float)Math.PI / 2.0f);
// 自動で回転
childBoxMatrix *= Matrix.RotationY(Environment.TickCount / 200.0f);
// 親のボックスの一番上にくっつくよう移動
childBoxMatrix *= Matrix.Translation(0.0f, 1.0f, 0.0f);
// 親のマトリックスを受け継ぐ
childBoxMatrix *= parentBoxMatrix;
// 座標変換
this._device.SetTransform(TransformType.World, childBoxMatrix);
// 子ボックス描画
this._box.DrawSubset(0);
// 文字列の描画
this.RenderText();
// 描画はここまで
this._device.EndScene();
// 実際のディスプレイに描画
this._device.Present();
}
<summary>
テキストの描画
</summary>
private void RenderText()
{
this.DrawText("[Escape]終了", 0, 0, Color.White);
this.DrawText("θ:" + this._lensPosTheta, 0, 12, Color.White);
this.DrawText("φ:" + this._lensPosPhi, 0, 24, Color.White);
this.DrawText("parentMatrix:" + this._parentBoxRotate.ToString(), 0, 36, Color.White);
}
<summary>
リソースの破棄をするために呼ばれる
</summary>
public void Dispose()
{
// 作成したメッシュの破棄
this.SafeDispose(this._box);
// リソースの破棄
this.DisposeResource();
}
}
}
MainSamplePartial.cs ファイルのコードはこちらです。
MainSamplePartial.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace MDXSample
{
public partial class MainSample
{
<summary>
メインフォーム
</summary>
private MainForm _form = null;
<summary>
Direct3D デバイス
</summary>
private Device _device = null;
<summary>
Direct3D 用フォント
</summary>
private Microsoft.DirectX.Direct3D.Font _font = null;
<summary>
キーのプレス判定
</summary>
private bool[] _keys = new bool[256];
<summary>
1つ前のマウスの位置
</summary>
private Point _oldMousePoint = Point.Empty;
<summary>
カメラレンズの位置(R)
</summary>
private float _lensPosRadius = 5.0f;
<summary>
カメラレンズの位置(θ)
</summary>
private float _lensPosTheta = 300.0f;
<summary>
カメラレンズの位置(φ)
</summary>
private float _lensPosPhi = 30.0f;
<summary>
XYZライン用頂点バッファ
</summary>
private VertexBuffer _xyzLineVertexBuffer = null;
<summary>
入力イベント作成
</summary>
<param name="topLevelForm">トップレベルウインドウ</param>
private void CreateInputEvent(MainForm topLevelForm)
{
// キーイベント作成
topLevelForm.KeyDown += new KeyEventHandler(this.form_KeyDown);
topLevelForm.KeyUp += new KeyEventHandler(this.form_KeyUp);
// マウス移動イベント
topLevelForm.MouseMove += new MouseEventHandler(this.form_MouseMove);
}
<summary>
キーボードのキーを押した瞬間
</summary>
<param name="sender"></param>
<param name="e"></param>
private void form_KeyDown(object sender, KeyEventArgs e)
{
// 押されたキーコードのフラグを立てる
if ((int)e.KeyCode < this._keys.Length)
{
this._keys[(int)e.KeyCode] = true;
}
}
<summary>
キーボードのキーを放した瞬間
</summary>
<param name="sender"></param>
<param name="e"></param>
private void form_KeyUp(object sender, KeyEventArgs e)
{
// 放したキーコードのフラグを下ろす
if ((int)e.KeyCode < this._keys.Length)
{
this._keys[(int)e.KeyCode] = false;
}
}
<summary>
マウス移動イベント
</summary>
<param name="sender"></param>
<param name="e"></param>
private void form_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
// 回転
this._lensPosTheta -= e.Location.X - this._oldMousePoint.X;
this._lensPosPhi += e.Location.Y - this._oldMousePoint.Y;
// φに関しては制限をつける
if (this._lensPosPhi >= 90.0f)
{
this._lensPosPhi = 89.9999f;
}
else if (this._lensPosPhi <= -90.0f)
{
this._lensPosPhi = -89.9999f;
}
}
// マウスの位置を記??
this._oldMousePoint = e.Location;
}
<summary>
Direct3D デバイスの作成
</summary>
<param name="topLevelForm">トップレベルウインドウ</param>
private void CreateDevice(MainForm topLevelForm)
{
// PresentParameters。デバイスを作成する際に必須
// どのような環境でデバイスを使用するかを設定する
PresentParameters pp = new PresentParameters();
// ウインドウモードなら true、フルスクリーンモードなら false を指定
pp.Windowed = true;
// スワップ効果。とりあえず「Discard」を指定。
pp.SwapEffect = SwapEffect.Discard;
// 深度ステンシルバッファ。3Dでは前後関係があるので通常 true
pp.EnableAutoDepthStencil = true;
// 自動深度ステンシル サーフェイスのフォーマット。
// 「D16」に対応しているビデオカードは多いが、前後関係の精度があまりよくない。
// できれば「D24S8」を指定したいところ。
pp.AutoDepthStencilFormat = DepthFormat.D16;
try
{
// デバイスの作成
this.CreateDevice(topLevelForm, pp);
}
catch (DirectXException ex)
{
// 例外発生
throw ex;
}
}
<summary>
Direct3D デバイスの作成
</summary>
<param name="topLevelForm">トップレベルウインドウ</param>
<param name="presentationParameters">PresentParameters 構造体</param>
private void CreateDevice(MainForm topLevelForm, PresentParameters presentationParameters)
{
// 実際にデバイスを作成します。
// 常に最高のパフォーマンスで作成を試み、
// 失敗したら下位パフォーマンスで作成するようにしている。
try
{
// ハードウェアによる頂点処理、ラスタライズを行う
// 最高のパフォーマンスで処理を行えます。
// ビデオカードによっては実装できない処理が存在します。
this._device = new Device(0, DeviceType.Hardware, topLevelForm.Handle,
CreateFlags.HardwareVertexProcessing, presentationParameters);
}
catch (DirectXException ex1)
{
// 作成に失敗
Debug.WriteLine(ex1.ToString());
try
{
// ソフトウェアによる頂点処理、ハードウェアによるラスタライズを行う
this._device = new Device(0, DeviceType.Hardware, topLevelForm.Handle,
CreateFlags.SoftwareVertexProcessing, presentationParameters);
}
catch (DirectXException ex2)
{
// 作成に失敗
Debug.WriteLine(ex2.ToString());
try
{
// ソフトウェアによる頂点処理、ラスタライズを行う
// パフォーマンスはとても低いです。
// その代わり、ほとんどの処理を制限なく行えます。
this._device = new Device(0, DeviceType.Reference, topLevelForm.Handle,
CreateFlags.SoftwareVertexProcessing, presentationParameters);
}
catch (DirectXException ex3)
{
// 作成に失敗
// 事実上デバイスは作成できません。
throw ex3;
}
}
}
// デバイスの状態変更イベントを作成
this._device.DeviceLost += new EventHandler(this.device_DeviceLost);
this._device.DeviceReset += new EventHandler(this.device_DeviceReset);
}
<summary>
フォントの作成
</summary>
private void CreateFont()
{
try
{
// フォントデータの構造体を作成
FontDescription fd = new FontDescription();
// 構造体に必要なデータをセット
fd.Height = 12;
fd.FaceName = "MS ゴシック";
// フォントを作成
this._font = new Microsoft.DirectX.Direct3D.Font(this._device, fd);
}
catch (DirectXException ex)
{
// 例外発生
throw ex;
}
}
<summary>
XYZライン作成
</summary>
private void CreateXYZLine()
{
// 6つ分の頂点を作成
this._xyzLineVertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionColored),
6, this._device, Usage.None, CustomVertex.PositionColored.Format, Pool.Managed);
// 頂点バッファをロックして、位置、色情報を書き込む
using (GraphicsStream data = this._xyzLineVertexBuffer.Lock(0, 0, LockFlags.None))
{
// 今回は各XYZのラインを原点(0.0f, 0.0f, 0.0f)からプラス方向に 10.0f 伸びた線を作成
data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 0.0f, Color.Red.ToArgb()));
data.Write(new CustomVertex.PositionColored(10.0f, 0.0f, 0.0f, Color.Red.ToArgb()));
data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 0.0f, Color.Green.ToArgb()));
data.Write(new CustomVertex.PositionColored(0.0f, 10.0f, 0.0f, Color.Green.ToArgb()));
data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 0.0f, Color.Blue.ToArgb()));
data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 10.0f, Color.Blue.ToArgb()));
this._xyzLineVertexBuffer.Unlock();
}
}
<summary>
カメラの位置をセット
</summary>
<param name="radius">半径(degree)</param>
<param name="theta">水平方向角度(degree)</param>
<param name="phi">垂直方向角度(degree)</param>
private void SetCameraPosition(float radius, float theta, float phi)
{
this._lensPosRadius = radius;
this._lensPosTheta = theta;
this._lensPosPhi = phi;
}
<summary>
ライトの設定
</summary>
private void SettingLight()
{
// 平行光線を使用
this._device.Lights[0].Type = LightType.Directional;
// ライトの方向
this._device.Lights[0].Direction = new Vector3(1.0f, -1.5f, 2.0f);
// 光の色は白
this._device.Lights[0].Diffuse = Color.White;
// 環境光
this._device.Lights[0].Ambient = Color.FromArgb(255, 128, 128, 128);
// 0 番のライトを有効
this._device.Lights[0].Enabled = true;
// 0 番のライトを更新
this._device.Lights[0].Update();
}
<summary>
カメラの設定
</summary>
private void SettingCamera()
{
// レンズの位置を三次元極座標で変換
float radius = this._lensPosRadius;
float theta = Geometry.DegreeToRadian(this._lensPosTheta);
float phi = Geometry.DegreeToRadian(this._lensPosPhi);
Vector3 lensPosition = new Vector3(
(float)(radius * Math.Cos(theta) * Math.Cos(phi)),
(float)(radius * Math.Sin(phi)),
(float)(radius * Math.Sin(theta) * Math.Cos(phi)));
// ビュー変換行列を設定
this._device.Transform.View = Matrix.LookAtLH(
lensPosition, new Vector3(0.0f, 0.0f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f));
// 射影変換を設定
this._device.Transform.Projection = Matrix.PerspectiveFovLH(
Geometry.DegreeToRadian(60.0f),
(float)this._device.Viewport.Width / (float)this._device.Viewport.Height,
1.0f, 100.0f);
}
<summary>
デバイスを保証する
</summary>
<returns></returns>
private bool EnsureDevice()
{
// デバイスが動作可能かチェック
int deviceResult;
if (!this._device.CheckCooperativeLevel(out deviceResult))
{
switch ((ResultCode)deviceResult)
{
case ResultCode.DeviceLost:
// まだリセットできる状態ではないので少し待つ
System.Threading.Thread.Sleep(10);
return false;
case ResultCode.DeviceNotReset:
// リセット可能状態
// デバイスをリセット
this._device.Reset(this._device.PresentationParameters);
return true;
default:
// 原因不明(正確には上記以外)
// まだリセットできる状態ではないので少し待つ
System.Threading.Thread.Sleep(10);
return false;
}
}
return true;
}
<summary>
XYZライン描画
</summary>
private void RenderXYZLine()
{
// ライトの状態を記憶
bool oldLightEnable = this._device.RenderState.Lighting;
// ライトを無効
if (oldLightEnable)
{
this._device.RenderState.Lighting = false;
}
// テクスチャー無効
this._device.SetTexture(0, null);
// 原点に配置
this._device.SetTransform(TransformType.World, Matrix.Identity);
// ストリームセット
this._device.SetStreamSource(0, this._xyzLineVertexBuffer, 0);
// 頂点フォーマットセット
this._device.VertexFormat = CustomVertex.PositionColored.Format;
// 描画
this._device.DrawPrimitives(PrimitiveType.LineList, 0, 3);
// ライトを戻す
if (oldLightEnable)
{
this._device.RenderState.Lighting = oldLightEnable;
}
}
<summary>
テキストの描画
</summary>
<param name="text">描画する文字列</param>
<param name="left">文字列の左位置</param>
<param name="top">文字列の上位置</param>
<param name="color">文字の色</param>
private void DrawText(string text, int left, int top, Color color)
{
this._font.DrawText(null, text, left, top, color);
}
<summary>
リソースの破棄
</summary>
private void DisposeResource()
{
// XYZラインの破棄
this.SafeDispose(this._xyzLineVertexBuffer);
// フォントのリソース解放
this.SafeDispose(this._font);
// Direct3D デバイスのリソース解放
this.SafeDispose(this._device);
}
<summary>
安全なリソース破棄
</summary>
<param name="resource">破棄するリソース</param>
private void SafeDispose(IDisposable resource)
{
if (resource != null)
{
resource.Dispose();
}
}
}
}
<summary>
ボックスメッシュ
</summary>
private Mesh _box = null;
<summary>
親ボックスの回転マトリックス
</summary>
private Matrix _parentBoxRotate = Matrix.Identity;
ボックスを描画するので、ボックス用のメッシュを宣言しておきます。2個描画していますが、同じメッシュを使っているので、作成するメッシュはひとつでいいです。
親のボックスの回転保持するために回転用の値を宣言しておきますが、型を「Matrix」で持つようにしています。X 軸と Z 軸の回転を行うので float を二つ持ち、「Matrix.RotateX」と「Matrix.RotateZ」で回転させればいいのですが、ローカル座標での操作が含まれるので、非常に操作しづらくなります。
そのためワールド座標で操作できるように初めから「Matrix」で値を持つようにしています。画面に表示されている軸に沿って回転するので操作が分かりやすいです。
今回 Matrix で値を持っていますが、回転しか使用していないので Quaternion でも代用可能です。分かる人は置き換えてみてもいいかもしれません。
// ボックス作成
this._box = Mesh.Box(this._device, 0.5f, 2.0f, 0.5f);
初期化メソッドではボックスを作成するだけです。特に新しいものはありません。縦長のボックスを作成します。
// 親ボックスの回転
if (this._keys[(int)Keys.Down])
{
this._parentBoxRotate *= Matrix.RotationX(-0.1f);
}
if (this._keys[(int)Keys.Up])
{
this._parentBoxRotate *= Matrix.RotationX(0.1f);
}
if (this._keys[(int)Keys.Left])
{
this._parentBoxRotate *= Matrix.RotationZ(0.1f);
}
if (this._keys[(int)Keys.Right])
{
this._parentBoxRotate *= Matrix.RotationZ(-0.1f);
}
親のボックスを回転できるようにキーボードでの操作を受け付けます。現在保持している回転マトリックスに生成した回転マトリックスを掛け合わせることにより、ワールド座標操作を可能にしています。
// マテリアル初期化
Material mtrl = new Material();
mtrl.Ambient = Color.FromArgb(255, 128, 128, 128);
// マテリアル
mtrl.Diffuse = Color.Red;
this._device.Material = mtrl;
親ボックスのマテリアルをセットしています。
// 親のマトリックス初期化
Matrix parentBoxMatrix = Matrix.Identity;
// 回転軸移動
parentBoxMatrix *= Matrix.Translation(0.0f, 1.0f, 0.0f);
// 回転
parentBoxMatrix *= this._parentBoxRotate;
// 座標変換
this._device.SetTransform(TransformType.World, parentBoxMatrix);
// 親ボックス描画
this._box.DrawSubset(0);
最初に親のボックスの計算と描画を行います。描画順は別にどちらから描画しても関係ないのですが、計算は先に親のボックスの方を行わなくてはなりません。なぜなら子のボックスの計算に親の座標変換マトリックスを使用するからです。
今回の Tips は計算がメインなので少し詳しく説明します。
まず、親用のマトリックスを宣言しておき、「Matrix.Identity」で初期化します。このマトリックスだと親のボックスは下の図のように配置されます。
1マス 1.0 とします。
最初に回転軸をボックスの底にしたいので「移動用マトリックス」を生成して掛け合わせます。ボックスを 1.0 Y軸方向に移動させます。
原点がボックスの下に来たので、そのまま回転軸にもなります。後はキーボード操作で計算した回転マトリックスを掛け合わせると、ボックスの底を中心にボックスが回転します。
親のボックスは子のボックスの計算の影響を受けません。むしろ親は、子の存在を意識する必要性はほとんどありません。
// マテリアル
mtrl.Diffuse = Color.Blue;
this._device.Material = mtrl;
// 子のマトリックス初期化
Matrix childBoxMatrix = Matrix.Identity;
// 回転軸移動
childBoxMatrix *= Matrix.Translation(0.0f, 1.0f, 0.0f);
// 初期回転位置(傾き)
childBoxMatrix *= Matrix.RotationZ((float)Math.PI / 2.0f);
// 自動で回転
childBoxMatrix *= Matrix.RotationY(Environment.TickCount / 200.0f);
// 親のボックスの一番上にくっつくよう移動
childBoxMatrix *= Matrix.Translation(0.0f, 1.0f, 0.0f);
// 親のマトリックスを受け継ぐ
childBoxMatrix *= parentBoxMatrix;
// 座標変換
this._device.SetTransform(TransformType.World, childBoxMatrix);
// 子ボックス描画
this._box.DrawSubset(0);
ここが今回の Tips のメインとなるコードです。
親子関係での計算で、親と子のマトリックスは下のように計算して算出します。
親ボックスの座標変換マトリックス | 親の座標変換マトリックス(ローカル) |
子ボックスの座標変換マトリックス | 子の座標変換マトリックス(ローカル)×親の座標変換マトリックス |
子は親の座標変換マトリックスを受け継ぎます。受け継ぐには子のローカル座標変換マトリックスの後に親のワールドマトリックスを掛け合わせればいいのです。前ではありません。気をつけてください。
なぜ、親のマトリックスをかけているかというと、下のような考えに基づいているからです。
- 親がX軸方向に移動したら、子もX軸方向に移動しましょう
- 親が90度回転したら、子も親の回転軸にあわせて90度回転移動しましょう
では、まず子のローカル座標変換マトリックスを図を使って順番に計算していきましょう。
最初は「Matrix.Identity」で初期化します。初期位置は親とまったく同じです。
次に回転軸をボックスの底にもっていきたいので、Y軸方向に 1.0 移動します。これも親と同じです。
このまま直立状態だとあまり面白くないので、思い切って90度傾けます。「Matrix.RotationZ」で2分のπを渡して横倒しします。
あとは自動でY軸回転するように「Matrix.RotationY」メソッドに「Environment.TickCount / 200.0f」で回転量を算出して、回転マトリックスを生成します。これを子のマトリックスに掛け合わせればぐるぐる回ります。(ちなみにここで生成したマトリックスで座標変換すれば、原点を中心にして回転させることが出来ます)
これが出来れば後は親のマトリックスを掛け合わせるだけです。しかし、ここで問題あります。親のマトリックスをかけるということは、子のローカル座標の原点が親のローカル座標の原点に重なる(回転などすべて含んでの意味)ということです。親のローカル座標の原点はボックスの中心です。下の図のようになります。
親のボックスの先端につけたいのに、これでは変ですね。なので、親のマトリックスを掛け合わせる前に、子ボックスのローカル座標計算でもう一度Y軸方向に 1.0 移動させておきます。
親のマトリックスをかければ下の図のようになります。
子のボックスのローカル座標の原点が、親のボックスのローカル座標の原点に重なるので、親のボックスが移動しようが回転しようが、子のボックスがついていくようになります。
今回、親子関係で行った計算方法は絶対的なものではありません。別な方法でも算出することもありますし、それはソフトの内容や、プログラマが決める仕様で変わることもあります。
しかし、今回紹介した方法は結構使われる計算ではあるので、覚えておいて損は無いと思います。