6

3D ベクターの非常に単純なクラス定義TVector3Dと、関数を実装するために使用されるいくつかのメソッドがありTVector3D.Normaliseます。Normalise関数に既に正規化されたベクトルを渡す場合、渡したベクトルを返すようにします。ここで私は使用Result := Selfしましたが、私はいくつかの狂ったリターンを持っています.

コンソール アプリケーション:

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  TVector3D = Class
  public
    x : Single;
    y : Single;
    z : Single;
    constructor Create(x : Single;
                       y : Single;
                       z : Single);
    function GetMagnitude() : Single;
    function IsUnitVector() : Boolean;
    function Normalise() : TVector3D;
  end;

  constructor TVector3D.Create(x : Single;
                               y : Single;
                               z : Single);
  begin
    Self.x := x;
    Self.y := y;
    Self.z := z;
  end;

  function TVector3D.GetMagnitude;
  begin
    Result := Sqrt(Sqr(Self.x) + Sqr(Self.y) + Sqr(Self.z));
  end;

  function TVector3D.IsUnitVector;
  begin
    if Self.GetMagnitude = 1 then
      Result := True
    else
      Result := False;
  end;

  function TVector3D.Normalise;
  var
    x : Single;
    y : Single;
    z : Single;
    MagnitudeFactor : Single;
  begin
    if IsUnitVector then
      Result := Self
    else
      MagnitudeFactor := 1/(Self.GetMagnitude);
      x := Self.x*MagnitudeFactor;
      y := Self.y*MagnitudeFactor;
      z := Self.z*MagnitudeFactor;
      Result := TVector3D.Create(x,
                                 y,
                                 z);
  end;

  procedure TestNormalise;
  var
    nonUnitVector : TVector3D;
    unitVector : TVector3D;
    nUVNormed : TVector3D;
    uVNormed : TVector3D;
  begin
  //Setup Vectors for Test
    nonUnitVector := TVector3D.Create(1,
                                      1,
                                      1);
    unitVector := TVector3D.Create(1,
                                   0,
                                   0);
  //Normalise Vectors & Free Memory
    nUVNormed := nonUnitVector.Normalise;
    nonUnitVector.Free;
    uVNormed := unitVector.Normalise;
    unitVector.Free;
  //Print Output & Free Memory
    WriteLn('nUVNormed = (' + FloatToStr(nUVNormed.x) + ', ' + FloatToStr(nUVNormed.y) + ', ' + FloatToStr(nUVNormed.z) + ')');
    nUVNormed.Free;
    WriteLn('uVNormed = (' + FloatToStr(uVNormed.x) + ', ' + FloatToStr(uVNormed.y) + ', ' + FloatToStr(uVNormed.z) + ')');
    uVNormed.Free;
  end;

begin
  try
    TestNormalise;
    Sleep(10000);
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Normalise非単位ベクトルに対して正常に動作します。つまり、IsUnitVectorfalse を返します。しかし、 などの単位ベクトルの場合、(1,0,0)それ自体を返す代わりに、 などの以前にゼロ以外の数値があった場合は常に、ゼロ以外の数値が非常に小さい結果が得られ(8.47122...E-38,0,0)ます。

Self を評価するように設定された行にブレークポイントを設定してデバッガーでこれを実行するとResult := Self、Self は(1,0,0)まだ結果になり(Very Low Number,0,0)ます。Very Low Numberプログラムを実行するたびに場所が変わりますが、常に周りにあるようE-38/E-39です。

なぜこれが起こるのかわかりません。なぜそれが起こるのか、そしてそれNormaliseを避けるために私の機能をどのように変更するのが最善なのか.

4

3 に答える 3

7

すべての問題の根本は、参照型であるクラスを使用していることです。代わりに、ベクターを値型にする必要があります。つまり、 を使用しrecordます。

あなたのコードでは、@NGLN によって特定された問題を修正したとしても、呼び出しを開始するまでにクラスのすべてのインスタンスを破棄していますWriteLn

この問題を早急に把握しないと、今後も問題が発生する恐れがあります。値型を使用するように切り替えると、現在のアプローチと比較してコーディングが簡単になります。

ここにあなたが始めるための何かがあります:

type
  TVector3 = record
  public
    class operator Negative(const V: TVector3): TVector3;
    class operator Equal(const V1, V2: TVector3): Boolean;
    class operator NotEqual(const V1, V2: TVector3): Boolean;
    class operator Add(const V1, V2: TVector3): TVector3;
    class operator Subtract(const V1, V2: TVector3): TVector3;
    class operator Multiply(const V: TVector3; const D: Double): TVector3;
    class operator Multiply(const D: Double; const V: TVector3): TVector3;
    class operator Divide(const V: TVector3; const D: Double): TVector3;
    class function New(const X, Y, Z: Double): TVector3; static;
    function IsZero: Boolean;
    function IsNonZero: Boolean;
    function IsUnit: Boolean;
    function Mag: Double;
    function SqrMag: Double;
    function Normalised: TVector3;
    function ToString: string;
  public
    X, Y, Z: Double;
  end;

const
  ZeroVector3: TVector3=();

class operator TVector3.Negative(const V: TVector3): TVector3;
begin
  Result.X := -V.X;
  Result.Y := -V.Y;
  Result.Z := -V.Z;
end;

class operator TVector3.Equal(const V1, V2: TVector3): Boolean;
begin
  Result := (V1.X=V2.X) and (V1.Y=V2.Y) and (V1.Z=V2.Z);
end;

class operator TVector3.NotEqual(const V1, V2: TVector3): Boolean;
begin
  Result := not (V1=V2);
end;

class operator TVector3.Add(const V1, V2: TVector3): TVector3;
begin
  Result.X := V1.X + V2.X;
  Result.Y := V1.Y + V2.Y;
  Result.Z := V1.Z + V2.Z;
end;

class operator TVector3.Subtract(const V1, V2: TVector3): TVector3;
begin
  Result.X := V1.X - V2.X;
  Result.Y := V1.Y - V2.Y;
  Result.Z := V1.Z - V2.Z;
end;

class operator TVector3.Multiply(const V: TVector3; const D: Double): TVector3;
begin
  Result.X := D*V.X;
  Result.Y := D*V.Y;
  Result.Z := D*V.Z;
end;

class operator TVector3.Multiply(const D: Double; const V: TVector3): TVector3;
begin
  Result.X := D*V.X;
  Result.Y := D*V.Y;
  Result.Z := D*V.Z;
end;

class operator TVector3.Divide(const V: TVector3; const D: Double): TVector3;
begin
  Result := (1.0/D)*V;
end;

class function TVector3.New(const X, Y, Z: Double): TVector3;
begin
  Result.X := X;
  Result.Y := Y;
  Result.Z := Z;
end;

function TVector3.IsZero: Boolean;
begin
  Result := Self=ZeroVector3;
end;

function TVector3.IsNonZero: Boolean;
begin
  Result := Self<>ZeroVector3;
end;

function TVector3.IsUnit: Boolean;
begin
  Result := abs(1.0-Mag)<1.0e-5;
end;

function TVector3.Mag: Double;
begin
  Result := Sqrt(X*X + Y*Y + Z*Z);
end;

function TVector3.SqrMag: Double;
begin
  Result := X*X + Y*Y + Z*Z;
end;

function TVector3.Normalised;
begin
  Result := Self/Mag;
end;

function TVector3.ToString: string;
begin
  Result := Format('(%g, %g, %g)', [X, Y, Z]);
end;

これは、私自身のコードベースから抽出されたものです。私は を使用してDoubleいますが、どうしても を使用したい場合Singleは、簡単に変更できます。

演算子のオーバーロードを使用すると、記述したコードが非常に読みやすくなります。などを書くことができるようV3 := V1 + V2になりました。

このレコードを使用したテスト コードは次のようになります。

var
  nonUnitVector: TVector3;
  unitVector: TVector3;
  nUVNormed: TVector3;
  uVNormed: TVector3;

begin
  //Setup Vectors for Test
  nonUnitVector := TVector3.New(1, 1, 1);
  unitVector := TVector3.New(1, 0, 0);

  //Normalise Vectors
  nUVNormed := nonUnitVector.Normalised;
  uVNormed := unitVector.Normalised;

  //Print Output
  WriteLn('nUVNormed = ' + nUVNormed.ToString);
  WriteLn('uVNormed = ' + uVNormed.ToString);
  Readln;
end.

または、多少圧縮したい場合:

WriteLn('nUVNormed = ' + TVector3.New(1, 1, 1).Normalised.ToString);
WriteLn('uVNormed = ' + TVector3.New(1, 0, 0).Normalised.ToString);
于 2013-01-27T16:00:39.323 に答える
7

現在のTVector3D.Normalise実装にはいくつかの問題があります。

  • , の後にブロックを使用していないため、最後の 4 行は常に実行されます。begin-endelse
  • したがって、ルーチンは を返すことはありませんがSelf、常に新しいインスタンスを返します。
  • 返されたインスタンスのメモリは、関数呼び出し後に所有権を失ったため、おそらくリークされています。
  • が をIsUnitVector返すTrueと、 の割り当てMagnitudeFactorがスキップされ、ランダムな値 (現在そのメモリのアドレスに存在する) になります。これについても、コンパイラから警告が表示されます。変数MagnitudeFactorが初期化されていない可能性があります。

代わりに、ルーチンを次のように書き直します。

function TVector3D.Normalise: TVector3D;
begin
  if not IsUnitVector then
  begin
    x := x / GetMagnitude;
    y := y / GetMagnitude;
    z := z / GetMagnitude;
  end;
  Result := Self;
end;
于 2013-01-27T14:06:47.343 に答える
6

いくつかのヒント:

まず、私があなたなら、ベクトルをクラスではなくレコードにしますが、YMMV. コンパイラはすべてのベクトルの有効期間を管理するため、これは大幅に簡素化されます (解放について心配する必要はありません)。2番、

function TVector3D.IsUnitVector;
begin
  if self.GetMagnitude = 1 then
    result := True
  else
    result := False;
end;

は通常、構文的にも正確に同等に書かれていますが、

function TVector3D.IsUnitVector;
begin
  result := GetMagnitude = 1
end;

しかし、そうであっても、それは正しくありません。浮動小数点数を扱っているため、等価性を確実にテストすることはできません。代わりに、「ファズ」が干渉しないように、マグニチュードが 1 の範囲内にあるかどうかを確認する必要があります。たとえば、次のことができます ( uses Math)

function TVector3D.IsUnitVector;
begin
  result := IsZero(GetMagnitude - 1)
end;

第 3 に、Normalize関数は、正規化が必要な場合は新しいベクター オブジェクトを返し、そうでない場合は同じオブジェクトを返します。それは非常に紛らわしいです。インスタンスがいくつあるかはわかりません。代わりに、これを手順にします。

procedure TVector3D.Normalize;
var
  norm: single;
begin
  norm := GetMagnitude;
  x := x / norm;
  y := y / norm;
  z := z / norm;
end;

第 4 に、なぜorsingleの代わりに使用するのですdoublereal?

第 5 に、NGLN が指摘したように (彼の回答に賛成票を投じてください!)、関数の一部でbegin...endブロックを忘れたため、最後の 4 行が常に実行されます! したがって、常に新しいベクター インスタンスを作成します。それでも、私のポイントは非常に重要です。元の関数は(ブロックを追加するだけの場合)、条件に応じて新しいインスタンスを返すか作成することを意図しています。これはかなりひどいことです。インスタンスの数がわからないためです! (そして、おそらくベクトルをリークし始めるでしょう...)elseNormalizebegin...endself

于 2013-01-27T13:43:48.363 に答える