公開:2010/11/19
画像をポリゴンにマッピングするとき、空間内のポリゴンの座標 (x, y, z) と 画像上の座標 (u, v) が必要になります。画像上の座標はポリゴンのそれと 区別するために画像の左上を原点とし、右側を u + , 下側を v + で表します。 四角形のポリゴンであれば、ポリゴンの座標と対応させる画像上の座標がそれぞれ 4個必要になります。
FMS では、以下のように指定します。
------------------------------------------------------------------
QUADS // 四角形を指定
TEXTURE takikawa200906_21 // 画像ファイル名
// u v x y z
0.25 0.25 707 0 707 // u, v, x, y, z の順
0.25 63.875 831 0 556
63.875 63.875 815 -162 556
63.875 0.25 694 -138 707
------------------------------------------------------------------
舟形多円錐図法(というらしいです)
正距円筒画像
正距円筒画像は、地球儀に張られている分割画像を平面に敷き詰めて、その隙間を 補完するような形に変形された画像といえます。
画像を眺めた時、赤道付近は実際の見え方に近いですが、上下45度付近 から次第に形が怪しくなって極付近は極端に横に広がった画像になります。
画像の左端を東経0度とすると、中心は東経180度。右端はまた0度と なりますが、画像の上辺では北極点が広がり、下辺は南極点が広がっている といった具合です。
ところが、この画像の左上を基準とする U, V 値が、
球の中心から球の表面の各点を見たときの北極点を基準とする角度とリニアに 対応しているという特徴があります。
たとえば、8192 * 4096 ピクセルの正距円筒画像上の点 ( U, V ) = ( 2048, 512 )には、 球の北極点から右に90度、下に22.5度の地点にあるピクセルが描画されています。
θU = 360 * 2048 / 8192 = 90, θV = 180 * 512 / 4096 = 22.5
上の画像は、キューブへ切り出すために変形した画像です。 わかりやすくするために赤い切取線を入れてあります。
この状態から上の三角形を組み合わせて _top 画像にします。 下の三角形が _bottom 画像になり、中央には左から _left、_front、_right が並び、左右の半幅の画像を組み合わせて _back 画像にします。
変形の実際をわかりやすくするために、赤と青の格子模様の画像を変形したものを 以下に示します。
この画像から、立方体各面の球と接する部分は画像が縮小され、 球との距離が大きくなる辺の部分では画像が拡大されていることがわかります。 また、頂点付近では拡大の度合いがより大きくなっています。
前述の、ピクセル値から角度を求める式から、 正距円筒画像の幅を W, 高さを H としたとき、画像上の点 (U, V)の角度 θU, θV は 以下の式で求められます。
θU = 360 * U / W ... (1)
θV = 180 * V / H ... (2)
また、角度からピクセル値を求める場合は
U = W * θU / 360 ... (3)
V = H * θV / 180 ... (4)
キューブに置き換えるときは、球に接する立方体の各面のすべてのピクセル について、球の中心からの角度を求め、それを上の式で U, V 変換すれば、 そこへもって来るべき正距円筒画像上のピクセルの位置が得られます。
変形後の画像は、下図のように緑の線で同じ形に4分割することが出来ますが、 その画像も赤の線で同じ形に4分割することが出来ます。 角度を求める計算は全体の16分の1になる赤い字の1の領域だけでよいことになります。 また、空白となる三角形部分は計算から除外しても良いこともわかります。
求めた計算結果を1から2へ反転コピーし、1と2から3と4へ反転コピーすることで、 必要な計算結果を得られます。この計算結果を利用して、4分の1ずつ画像全体を処理します。
赤1の領域について、立方体表面から球の原点を見たときの立体的な画像を示します。 画像では、球の45度を示すラインが、立方体の上面に現れることがわかります。
角度を求める計算式を示します。もっとエレガントな方法があるような 気もしますが、上図の上の三角形と下の正方形に分けて考えます。 球の半径を R (正方形の1辺の長さ)とします。
上の三角形上の任意の点 P と球の原点 O の角度を求めます。
tan(θU) = X / Y より、
θU = tan-1 ( X / Y) ... (5)
AP = √( X2 + Y2)
tan(θV) = AP / R より、
θV = tan-1 ( √( X2 + Y2) / R) ... (6)
次に、下の正方形上の任意の点 P と球の原点 O の角度を求めます。
tan(θU) = X / R より、
θU = tan-1 ( X / R) ... (7)
QP = √( X2 + R2)
OQ = R - Y
tan(θV) = QP / OQ より、
θV = tan-1 ( √( X2 + R2) / (R - Y)) ... (8)
以下は、pano2cube.exe で使われているサンプルコードです。 参考になればどうぞ。
------------------------------------------------------------------------------------------- unit Pano2Cube; interface uses Windows, Classes, Graphics; procedure TransformPano2Cube(Bitmap: TBitmap); implementation uses Math; type TCurrencyUV = record iU: Integer; dU: Currency; iV: Integer; dV: Currency; end; TmxCurrencyUV = array of array of TCurrencyUV; procedure InitmxUV(R: Integer; mx: TmxCurrencyUV); var R2, R4: Integer; U, V: Integer; E: Extended; begin R2 := R * 2; R4 := R * 4; // -1 でクリアする for V := 0 to R4 - 1 do for U := 0 to R2 - 1 do begin mx[U, V].iU := -1; mx[U, V].dU := -1; mx[U, V].iV := -1; mx[U, V].dV := -1; end; // 上面の三角形(1) for V := 0 to R - 1 do for U := 0 to V do begin // iU, dU if V = 0 then begin // 三角形の頂点 mx[U, V].iU := 0; mx[U, V].dU := 0; end else begin E := (ArcTan(U / V) * 180 / Pi) * R / 45; mx[U, V].iU := Trunc(E); mx[U, V].dU := E - Trunc(E); end; // iV, dV E := (ArcTan(Sqrt(U * U + V * V) / R) * 180 / Pi) * R / 45; mx[U, V].iV := Trunc(E); mx[U, V].dV := E - Trunc(E); end; // 後面の四角形(2) for V := R to R2 - 1 do for U := 0 to R - 1 do begin // iU, dU E := (ArcTan(U / R) * 180 / Pi) * R / 45; mx[U, V].iU := Trunc(E); mx[U, V].dU := E - Trunc(E); // iV, dV E := (ArcTan(Sqrt(R * R + U * U) / (R2 - V)) * 180 / Pi) * R / 45; mx[U, V].iV := Trunc(E); mx[U, V].dV := E - Trunc(E); end; // コピーする for U := R to R2 - 1 do for V := 0 to R2 - 1 do begin // (1)(2)-> (3)(4) // 左右反転 if mx[R2 - 1 - U, V].iU = -1 then mx[U, V].iU := -1 else begin if mx[R2 - 1 - U, V].dU = 0 then begin // 小数点以下がゼロの場合 mx[U, V].iU := R2 - 1 - mx[R2 - 1 - U, V].iU; mx[U, V].dU := mx[R2 - 1 - U, V].dU; end else begin // Bilinear 線形補間では左上から右下へ処理するので、 // 補正する mx[U, V].iU := R2 - 1 - mx[R2 - 1 - U, V].iU - 1; mx[U, V].dU := 1 - mx[R2 - 1 - U, V].dU; end; end; mx[U, V].iV := mx[R2 - 1 - U, V].iV; mx[U, V].dV := mx[R2 - 1 - U, V].dV; end; for U := 0 to R2 - 1 do for V := R2 to R4 - 1 do begin // (2)(4)-> (5)(7) // (1)(3)-> (6)(8) // 上下反転 mx[U, V].iU := mx[U, R4 - 1 - V].iU; mx[U, V].dU := mx[U, R4 - 1 - V].dU; if mx[U, R4 - 1 - V].dV = 0 then begin // 小数点以下がゼロの場合 mx[U, V].iV := R4 - 1 - mx[U, R4 - 1 - V].iV; mx[U, V].dV := mx[U, R4 - 1 - V].dV; end else begin // 線形補間用補正 mx[U, V].iV := R4 - 1 - mx[U, R4 - 1 - V].iV - 1; mx[U, V].dV := 1 - mx[U, R4 - 1 - V].dV; end; end; end; { --------------------------------------------------------- #MovePixels 元画像の4分の1を立方体画像へ変形させる http://www.rainorshine.asia/2008/05/16/post428.html による Bilinear 線形補間を利用する // windows.pas tagRGBQUAD = packed record rgbBlue: Byte; rgbGreen: Byte; rgbRed: Byte; rgbReserved: Byte; end; TRGBQuad = tagRGBQUAD; PRGBQuad = ^TRGBQuad; // classes.pas PPointerList = ^TPointerList; TPointerList = array[0..MaxListSize - 1] of Pointer; --------------------------------------------------------- } type // for pf32bit TQuadLine= array[0..MaxInt div SizeOf(TRGBQuad) - 1] of TRGBQuad; PQuadLine = ^TQuadLine; // for pf24bit TRGBTri = packed record rgbBlue: Byte; rgbGreen: Byte; rgbRed: Byte; end; PRGBTri = ^TRGBTri; TTriLine = array[0..MaxInt div SizeOf(TRGBTri) - 1] of TRGBTri; PTriLine = ^TTriLine; procedure MovePixels(Dest, Source: TBitmap; mx: TmxCurrencyUV); var W, H, U, V: Integer; pSourceLines, pDestLines: PPointerList; // Classes.pas PixelFormat: TPixelFormat; dq, sq00, sq01, sq10, sq11: TRGBQuad; dt, st00, st01, st10, st11: TRGBTri; sx0, sx1, sy0, sy1: Integer; rx0, rx1, ry0, ry1: Currency; r0, r1: Currency; g0, g1: Currency; b0, b1: Currency; begin PixelFormat := Source.PixelFormat; W := Source.Width; H := Source.Height; Dest.Width := W; Dest.Height := H; Dest.PixelFormat := PixelFormat; GetMem(pDestLines, H * SizeOf(Pointer)); try GetMem(pSourceLines, H * SizeOf(Pointer)); try // scanline for V := 0 to H - 1 do begin pDestLines[V] := Dest.ScanLine[V]; pSourceLines[V] := Source.ScanLine[V]; end; for V := 0 to H - 1 do begin for U := 0 to W - 1 do if mx[U, V].iU <> -1 then begin sy0 := mx[U, V].iV; // V データ整数部 sy1 := Min(sy0 + 1, H - 1); // 1ピクセル下 の V sx0 := mx[U, V].iU; // U データ整数部 sx1 := Min(sx0 + 1, W - 1); // 1ピクセル右の U ry1 := mx[U, V].dV; // 1ピクセル下から取得する割合 ry0 := 1 - ry1; // V から取得する割合 rx1 := mx[U, V].dU; // 1ピクセル右から取得する割合 rx0 := 1 - rx1; // U から取得する割合 case PixelFormat of pf32bit: begin {---------------------------------------------------- // 整数部だけの処理 // PQuadLine(pDestLines^[V])^[U] := PQuadLine(pSourceLines^[mx[U, V].iV])^[mx[U, V].iU]; Dest Source sx0 sx1 U iU iU + 1 *-------* *-------*-------* V | dq | sy0 iV | sq00 | sq10 | *-------* *-------*-------* sy1 iV + 1 | sq01 | sq11 | *-------*-------* ----------------------------------------------------} // dq := PQuadLine(pDestLines^[V])^[U]; sq00 := PQuadLine(pSourceLines^[sy0])^[sx0]; sq01 := PQuadLine(pSourceLines^[sy1])^[sx0]; sq10 := PQuadLine(pSourceLines^[sy0])^[sx1]; sq11 := PQuadLine(pSourceLines^[sy1])^[sx1]; // R, G, B を右隣と合成 r0 := sq00.rgbRed * rx0 + sq10.rgbRed * rx1; r1 := sq01.rgbRed * rx0 + sq11.rgbRed * rx1; g0 := sq00.rgbGreen * rx0 + sq10.rgbGreen * rx1; g1 := sq01.rgbGreen * rx0 + sq11.rgbGreen * rx1; b0 := sq00.rgbBlue * rx0 + sq10.rgbBlue * rx1; b1 := sq01.rgbBlue * rx0 + sq11.rgbBlue * rx1; dq.rgbRed := Min($FF, Round((r0 * ry0 + r1 * ry1))); dq.rgbGreen := Min($FF, Round((g0 * ry0 + g1 * ry1))); dq.rgbBlue := Min($FF, Round((b0 * ry0 + b1 * ry1))); PQuadLine(pDestLines^[V])^[U] := dq; end; pf24bit: begin {---------------------------------------------------- // 整数部だけの処理 // PTriLine(pDestLines^[V])^[U] := PTriLine(pSourceLines^[mx[U, V].iV])^[mx[U, V].iU]; Dest Source sx0 sx1 U iU iU + 1 *-------* *-------*-------* V | dt | sy0 iV | st00 | st10 | *-------* *-------*-------* sy1 iV + 1 | st01 | st11 | *-------*-------* ----------------------------------------------------} // dt := PQuadLine(pDestLines^[V])^[U]; st00 := PTriLine(pSourceLines^[sy0])^[sx0]; st01 := PTriLine(pSourceLines^[sy1])^[sx0]; st10 := PTriLine(pSourceLines^[sy0])^[sx1]; st11 := PTriLine(pSourceLines^[sy1])^[sx1]; // R, G, B を右隣と合成 r0 := st00.rgbRed * rx0 + st10.rgbRed * rx1; r1 := st01.rgbRed * rx0 + st11.rgbRed * rx1; g0 := st00.rgbGreen * rx0 + st10.rgbGreen * rx1; g1 := st01.rgbGreen * rx0 + st11.rgbGreen * rx1; b0 := st00.rgbBlue * rx0 + st10.rgbBlue * rx1; b1 := st01.rgbBlue * rx0 + st11.rgbBlue * rx1; dt.rgbRed := Min($FF, Round((r0 * ry0 + r1 * ry1))); dt.rgbGreen := Min($FF, Round((g0 * ry0 + g1 * ry1))); dt.rgbBlue := Min($FF, Round((b0 * ry0 + b1 * ry1))); PTriLine(pDestLines^[V])^[U] := dt; end; end; end; end; // for V finally FreeMem(pSourceLines); end; finally FreeMem(pDestLines); end; end; procedure TransformPano2Cube(Bitmap: TBitmap); var W, H, R, I: Integer; S, D: TBitmap; SR, DR: TRect; mx: TmxCurrencyUV; begin W := Bitmap.Width div 4; H := Bitmap.Height; S := TBitmap.Create; try S.PixelFormat := pf24bit; S.Width := W; S.Height := H; D := TBitmap.Create; try D.PixelFormat := pf24bit; D.Width := W; D.Height := H; R := W div 2; SetLength(mx, R * 2, R * 4); try InitmxUV(W div 2, mx); for I := 0 to 3 do begin DR := S.Canvas.ClipRect; SR := Rect(W * I, 0, W * (I + 1), H); S.Canvas.CopyRect(DR, Bitmap.Canvas, SR); MovePixels(D, S, mx); Bitmap.Canvas.CopyRect(SR, D.Canvas, DR); end; finally Finalize(mx); end; finally D.Free; end; finally S.Free; end; end; end. -------------------------------------------------------------------------------------------